feat(logging): migrate AI tasks to ai channel

This commit is contained in:
F4ilji
2026-07-05 23:29:39 +05:00
parent 49dd578e50
commit cd9b682994
7 changed files with 51 additions and 51 deletions
@@ -20,13 +20,13 @@ class CallAiServiceForFileSelectionTask
*/ */
public function run(array $fragments): ?array public function run(array $fragments): ?array
{ {
Log::info('[CallAiServiceForFileSelectionTask] Начало LLM запроса', [ Log::channel('ai')->info('Начало LLM запроса', [
'fragments_count' => count($fragments), 'fragments_count' => count($fragments),
]); ]);
$credential = $this->getCredentialTask->run('qwen'); $credential = $this->getCredentialTask->run('qwen');
if (!$credential) { if (!$credential) {
Log::error('[CallAiServiceForFileSelectionTask] API ключ не настроен', [ Log::channel('ai')->error('API ключ не настроен', [
'provider' => 'qwen', 'provider' => 'qwen',
]); ]);
return null; return null;
@@ -35,7 +35,7 @@ class CallAiServiceForFileSelectionTask
$prompt = $this->buildPrompt($fragments); $prompt = $this->buildPrompt($fragments);
Log::info('[CallAiServiceForFileSelectionTask] Отправка запроса к AI'); Log::channel('ai')->info('Отправка запроса к AI');
try { try {
$response = Http::timeout(30) // Таймаут 30 секунд $response = Http::timeout(30) // Таймаут 30 секунд
@@ -52,7 +52,7 @@ class CallAiServiceForFileSelectionTask
'max_tokens' => 500, 'max_tokens' => 500,
]); ]);
Log::info('[CallAiServiceForFileSelectionTask] Ответ от AI', [ Log::channel('ai')->info('Ответ от AI', [
'status' => $response->status(), 'status' => $response->status(),
'successful' => $response->successful(), 'successful' => $response->successful(),
]); ]);
@@ -61,19 +61,19 @@ class CallAiServiceForFileSelectionTask
$result = $response->json(); $result = $response->json();
$llmResponse = json_decode($result['choices'][0]['message']['content'], true); $llmResponse = json_decode($result['choices'][0]['message']['content'], true);
Log::info('[CallAiServiceForFileSelectionTask] Распознан ответ', [ Log::channel('ai')->info('Распознан ответ', [
'main_file' => $llmResponse['main_file'] ?? null, 'main_file' => $llmResponse['main_file'] ?? null,
]); ]);
return $llmResponse; return $llmResponse;
} else { } else {
Log::error('[CallAiServiceForFileSelectionTask] Ошибка AI запроса', [ Log::channel('ai')->error('Ошибка AI запроса', [
'status' => $response->status(), 'status' => $response->status(),
'body' => $response->body(), 'body' => $response->body(),
]); ]);
} }
} catch (\Exception $e) { } catch (\Exception $e) {
Log::error('[CallAiServiceForFileSelectionTask] Исключение при запросе к AI', [ Log::channel('ai')->error('Исключение при запросе к AI', [
'message' => $e->getMessage(), 'message' => $e->getMessage(),
'trace' => $e->getTraceAsString(), 'trace' => $e->getTraceAsString(),
]); ]);
@@ -23,14 +23,14 @@ class CallAiServiceTask
public function run(string $text, Collection $categories): ?array public function run(string $text, Collection $categories): ?array
{ {
try { try {
Log::info('[CallAiServiceTask] Начало AI запроса', [ Log::channel('ai')->info('Начало AI запроса', [
'text_length' => strlen($text), 'text_length' => strlen($text),
'categories_count' => $categories->count(), 'categories_count' => $categories->count(),
]); ]);
$credential = $this->getCredentialTask->run('qwen'); $credential = $this->getCredentialTask->run('qwen');
if (!$credential) { if (!$credential) {
Log::error('[CallAiServiceTask] API ключ не настроен', [ Log::channel('ai')->error('API ключ не настроен', [
'provider' => 'qwen', 'provider' => 'qwen',
]); ]);
throw new \RuntimeException('AI API ключ не настроен'); throw new \RuntimeException('AI API ключ не настроен');
@@ -43,7 +43,7 @@ class CallAiServiceTask
$systemPrompt = $this->buildSystemPrompt($categoryList); $systemPrompt = $this->buildSystemPrompt($categoryList);
Log::info('[CallAiServiceTask] Отправка запроса к AI', [ Log::channel('ai')->info('Отправка запроса к AI', [
'url' => 'https://routerai.ru/api/v1/chat/completions', 'url' => 'https://routerai.ru/api/v1/chat/completions',
'model' => 'qwen/qwen3.5-flash-02-23', 'model' => 'qwen/qwen3.5-flash-02-23',
]); ]);
@@ -58,7 +58,7 @@ class CallAiServiceTask
if ($response && $response->successful()) { if ($response && $response->successful()) {
return false; return false;
} }
Log::warning('[CallAiServiceTask] Попытка не удалась, повтор...', [ Log::channel('ai')->warning('Попытка не удалась, повтор...', [
'exception' => $exception->getMessage(), 'exception' => $exception->getMessage(),
]); ]);
return true; return true;
@@ -75,7 +75,7 @@ class CallAiServiceTask
'max_tokens' => 2000, 'max_tokens' => 2000,
]); ]);
Log::info('[CallAiServiceTask] Ответ от AI', [ Log::channel('ai')->info('Ответ от AI', [
'status' => $response->status(), 'status' => $response->status(),
'successful' => $response->successful(), 'successful' => $response->successful(),
]); ]);
@@ -85,7 +85,7 @@ class CallAiServiceTask
$content = $result['choices'][0]['message']['content'] ?? null; $content = $result['choices'][0]['message']['content'] ?? null;
if (empty($content)) { if (empty($content)) {
Log::error('[CallAiServiceTask] Пустой ответ от AI', [ Log::channel('ai')->error('Пустой ответ от AI', [
'response' => $result, 'response' => $result,
]); ]);
return null; return null;
@@ -94,14 +94,14 @@ class CallAiServiceTask
$data = json_decode($content, true); $data = json_decode($content, true);
if (json_last_error() !== JSON_ERROR_NONE) { if (json_last_error() !== JSON_ERROR_NONE) {
Log::error('[CallAiServiceTask] Ошибка парсинга JSON ответа', [ Log::channel('ai')->error('Ошибка парсинга JSON ответа', [
'error' => json_last_error_msg(), 'error' => json_last_error_msg(),
'content' => substr($content, 0, 500), 'content' => substr($content, 0, 500),
]); ]);
return null; return null;
} }
Log::info('[CallAiServiceTask] AI данные успешно распознаны', [ Log::channel('ai')->info('AI данные успешно распознаны', [
'title' => $data['title'] ?? 'N/A', 'title' => $data['title'] ?? 'N/A',
'has_body' => isset($data['body']), 'has_body' => isset($data['body']),
'category_id' => $data['category_id'] ?? 'N/A', 'category_id' => $data['category_id'] ?? 'N/A',
@@ -110,7 +110,7 @@ class CallAiServiceTask
return $data; return $data;
} }
Log::error('[CallAiServiceTask] AI запрос не удался', [ Log::channel('ai')->error('AI запрос не удался', [
'status' => $response->status(), 'status' => $response->status(),
'body' => $response->body(), 'body' => $response->body(),
'headers' => $response->headers(), 'headers' => $response->headers(),
@@ -118,7 +118,7 @@ class CallAiServiceTask
return null; return null;
} catch (\Exception $e) { } catch (\Exception $e) {
Log::error('[CallAiServiceTask] Критическая ошибка AI запроса', [ Log::channel('ai')->error('Критическая ошибка AI запроса', [
'error' => $e->getMessage(), 'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(), 'trace' => $e->getTraceAsString(),
]); ]);
@@ -17,13 +17,13 @@ class ConvertDocToDocxTask
*/ */
public function run(UploadedFile $file): ?string public function run(UploadedFile $file): ?string
{ {
Log::info('[ConvertDocToDocxTask] Начало конвертации', [ Log::channel('ai')->info('Начало конвертации', [
'file' => $file->getClientOriginalName(), 'file' => $file->getClientOriginalName(),
'extension' => $file->getClientOriginalExtension(), 'extension' => $file->getClientOriginalExtension(),
]); ]);
if (strtolower($file->getClientOriginalExtension()) !== 'doc') { if (strtolower($file->getClientOriginalExtension()) !== 'doc') {
Log::warning('[ConvertDocToDocxTask] Неверное расширение', ['extension' => $file->getClientOriginalExtension()]); Log::channel('ai')->warning('Неверное расширение', ['extension' => $file->getClientOriginalExtension()]);
return null; return null;
} }
@@ -32,7 +32,7 @@ class ConvertDocToDocxTask
$absoluteOriginalPath = Storage::disk('local')->path($originalPath); $absoluteOriginalPath = Storage::disk('local')->path($originalPath);
if (!file_exists($absoluteOriginalPath)) { if (!file_exists($absoluteOriginalPath)) {
Log::error('[ConvertDocToDocxTask] Файл не сохранен', ['path' => $absoluteOriginalPath]); Log::channel('ai')->error('Файл не сохранен', ['path' => $absoluteOriginalPath]);
return null; return null;
} }
@@ -50,7 +50,7 @@ class ConvertDocToDocxTask
escapeshellarg($absoluteOriginalPath) escapeshellarg($absoluteOriginalPath)
); );
Log::info('[ConvertDocToDocxTask] Выполнение команды', ['command' => $command]); Log::channel('ai')->info('Выполнение команды', ['command' => $command]);
$output = shell_exec($command); $output = shell_exec($command);
@@ -63,7 +63,7 @@ class ConvertDocToDocxTask
} }
if (!file_exists($docxPath)) { if (!file_exists($docxPath)) {
Log::error('[ConvertDocToDocxTask] Конвертация не удалась', [ Log::channel('ai')->error('Конвертация не удалась', [
'file' => $file->getClientOriginalName(), 'file' => $file->getClientOriginalName(),
'output' => $output, 'output' => $output,
'expected_path' => $docxPath, 'expected_path' => $docxPath,
@@ -78,7 +78,7 @@ class ConvertDocToDocxTask
$relativeDocxPath = preg_replace('/\.doc$/i', '.docx', $originalPath); $relativeDocxPath = preg_replace('/\.doc$/i', '.docx', $originalPath);
Log::info('[ConvertDocToDocxTask] Конвертация успешна', ['result_path' => $relativeDocxPath]); Log::channel('ai')->info('Конвертация успешна', ['result_path' => $relativeDocxPath]);
return $relativeDocxPath; return $relativeDocxPath;
} }
@@ -13,7 +13,7 @@ class ExtractTextFragmentTask
*/ */
public function run(UploadedFile $file, int $maxLength = 300): string public function run(UploadedFile $file, int $maxLength = 300): string
{ {
Log::info('[ExtractTextFragmentTask] Начало извлечения фрагмента', [ Log::channel('ai')->info('Начало извлечения фрагмента', [
'file' => $file->getClientOriginalName(), 'file' => $file->getClientOriginalName(),
'extension' => $file->getClientOriginalExtension(), 'extension' => $file->getClientOriginalExtension(),
]); ]);
@@ -30,7 +30,7 @@ class ExtractTextFragmentTask
return $this->extractFromDoc($file, $maxLength); return $this->extractFromDoc($file, $maxLength);
} }
Log::warning('[ExtractTextFragmentTask] Неизвестный формат', [ Log::channel('ai')->warning('Неизвестный формат', [
'extension' => $extension, 'extension' => $extension,
]); ]);
@@ -23,7 +23,7 @@ class ExtractTextFromDocumentTask
{ {
$extension = strtolower($file->getClientOriginalExtension()); $extension = strtolower($file->getClientOriginalExtension());
Log::info('[ExtractTextFromDocumentTask] Начало извлечения текста', [ Log::channel('ai')->info('Начало извлечения текста', [
'file' => $file->getClientOriginalName(), 'file' => $file->getClientOriginalName(),
'extension' => $extension, 'extension' => $extension,
'size' => $file->getSize(), 'size' => $file->getSize(),
@@ -31,17 +31,17 @@ class ExtractTextFromDocumentTask
// Если DOCX — парсим напрямую // Если DOCX — парсим напрямую
if ($extension === 'docx') { if ($extension === 'docx') {
Log::info('[ExtractTextFromDocumentTask] Обработка DOCX файла'); Log::channel('ai')->info('Обработка DOCX файла');
return $this->extractFromDocx($file); return $this->extractFromDocx($file);
} }
// Если DOC — конвертируем в DOCX, затем парсим // Если DOC — конвертируем в DOCX, затем парсим
if ($extension === 'doc') { if ($extension === 'doc') {
Log::info('[ExtractTextFromDocumentTask] Обработка DOC файла (требуется конвертация)'); Log::channel('ai')->info('Обработка DOC файла (требуется конвертация)');
return $this->extractFromDoc($file); return $this->extractFromDoc($file);
} }
Log::warning('[ExtractTextFromDocumentTask] Неизвестный формат файла', [ Log::channel('ai')->warning('Неизвестный формат файла', [
'extension' => $extension, 'extension' => $extension,
]); ]);
@@ -53,13 +53,13 @@ class ExtractTextFromDocumentTask
*/ */
private function extractFromDocx(UploadedFile $file): ?string private function extractFromDocx(UploadedFile $file): ?string
{ {
Log::info('[ExtractTextFromDocumentTask:extractFromDocx] Начало'); Log::channel('ai')->info('Начало');
// Сохраняем во временную директорию // Сохраняем во временную директорию
$tempPath = $file->storeAs('temp', 'temp_doc_' . time() . '.docx', 'local'); $tempPath = $file->storeAs('temp', 'temp_doc_' . time() . '.docx', 'local');
$absoluteTempPath = Storage::disk('local')->path($tempPath); $absoluteTempPath = Storage::disk('local')->path($tempPath);
Log::info('[ExtractTextFromDocumentTask:extractFromDocx] Файл сохранен', [ Log::channel('ai')->info('Файл сохранен', [
'path' => $tempPath, 'path' => $tempPath,
'absolute_path' => $absoluteTempPath, 'absolute_path' => $absoluteTempPath,
]); ]);
@@ -67,7 +67,7 @@ class ExtractTextFromDocumentTask
// Парсим файл // Парсим файл
$extractedText = $this->parseDocxTask->run($absoluteTempPath); $extractedText = $this->parseDocxTask->run($absoluteTempPath);
Log::info('[ExtractTextFromDocumentTask:extractFromDocx] Результат парсинга', [ Log::channel('ai')->info('Результат парсинга', [
'text_length' => strlen($extractedText ?? ''), 'text_length' => strlen($extractedText ?? ''),
]); ]);
@@ -82,19 +82,19 @@ class ExtractTextFromDocumentTask
*/ */
private function extractFromDoc(UploadedFile $file): ?string private function extractFromDoc(UploadedFile $file): ?string
{ {
Log::info('[ExtractTextFromDocumentTask:extractFromDoc] Начало конвертации'); Log::channel('ai')->info('Начало конвертации');
// Конвертируем DOC → DOCX // Конвертируем DOC → DOCX
$docxPath = $this->convertDocToDocxTask->run($file); $docxPath = $this->convertDocToDocxTask->run($file);
if (!$docxPath) { if (!$docxPath) {
Log::error('[ExtractTextFromDocumentTask:extractFromDoc] Не удалось конвертировать DOC файл', [ Log::channel('ai')->error('Не удалось конвертировать DOC файл', [
'file' => $file->getClientOriginalName(), 'file' => $file->getClientOriginalName(),
]); ]);
return null; return null;
} }
Log::info('[ExtractTextFromDocumentTask:extractFromDoc] Конвертация успешна', [ Log::channel('ai')->info('Конвертация успешна', [
'docx_path' => $docxPath, 'docx_path' => $docxPath,
]); ]);
@@ -104,7 +104,7 @@ class ExtractTextFromDocumentTask
// Парсим конвертированный файл // Парсим конвертированный файл
$extractedText = $this->parseDocxTask->run($absoluteDocxPath); $extractedText = $this->parseDocxTask->run($absoluteDocxPath);
Log::info('[ExtractTextFromDocumentTask:extractFromDoc] Результат парсинга', [ Log::channel('ai')->info('Результат парсинга', [
'text_length' => strlen($extractedText ?? ''), 'text_length' => strlen($extractedText ?? ''),
]); ]);
@@ -26,7 +26,7 @@ class FindMainNewsFileTask
*/ */
public function run(Collection $files): ?UploadedFile public function run(Collection $files): ?UploadedFile
{ {
Log::info('[FindMainNewsFileTask] Начало поиска основного файла', [ Log::channel('ai')->info('Начало поиска основного файла', [
'files_count' => $files->count(), 'files_count' => $files->count(),
'files' => $files->map(function($f) { 'files' => $files->map(function($f) {
return $f->getClientOriginalName() . ' (' . $f->getClientOriginalExtension() . ')'; return $f->getClientOriginalName() . ' (' . $f->getClientOriginalExtension() . ')';
@@ -61,19 +61,19 @@ class FindMainNewsFileTask
return false; return false;
}); });
Log::info('[FindMainNewsFileTask] Найдено DOC/DOCX файлов', [ Log::channel('ai')->info('Найдено DOC/DOCX файлов', [
'count' => $docFiles->count(), 'count' => $docFiles->count(),
'files' => $docFiles->map(fn($f) => $f->getClientOriginalName())->toArray(), 'files' => $docFiles->map(fn($f) => $f->getClientOriginalName())->toArray(),
]); ]);
if ($docFiles->isEmpty()) { if ($docFiles->isEmpty()) {
Log::warning('[FindMainNewsFileTask] Не найдено DOC/DOCX файлов'); Log::channel('ai')->warning('Не найдено DOC/DOCX файлов');
return null; return null;
} }
// Если только один файл — используем его // Если только один файл — используем его
if ($docFiles->count() === 1) { if ($docFiles->count() === 1) {
Log::info('[FindMainNewsFileTask] Найден один файл, используем его', [ Log::channel('ai')->info('Найден один файл, используем его', [
'file' => $docFiles->first()->getClientOriginalName(), 'file' => $docFiles->first()->getClientOriginalName(),
]); ]);
return $docFiles->first(); return $docFiles->first();
@@ -84,7 +84,7 @@ class FindMainNewsFileTask
$filename = mb_strtolower($file->getClientOriginalName()); $filename = mb_strtolower($file->getClientOriginalName());
foreach (self::NEWS_KEYWORDS as $keyword) { foreach (self::NEWS_KEYWORDS as $keyword) {
if (str_contains($filename, $keyword)) { if (str_contains($filename, $keyword)) {
Log::info('[FindMainNewsFileTask] Найден файл по ключевому слову', [ Log::channel('ai')->info('Найден файл по ключевому слову', [
'file' => $file->getClientOriginalName(), 'file' => $file->getClientOriginalName(),
'keyword' => $keyword, 'keyword' => $keyword,
]); ]);
@@ -93,7 +93,7 @@ class FindMainNewsFileTask
} }
} }
Log::info('[FindMainNewsFileTask] Не найдено файлов по ключевым словам, используем LLM'); Log::channel('ai')->info('Не найдено файлов по ключевым словам, используем LLM');
// Если не нашли по ключевым словам — используем LLM // Если не нашли по ключевым словам — используем LLM
return $this->findViaLLM($docFiles); return $this->findViaLLM($docFiles);
@@ -104,7 +104,7 @@ class FindMainNewsFileTask
*/ */
private function findViaLLM(Collection $docFiles): ?UploadedFile private function findViaLLM(Collection $docFiles): ?UploadedFile
{ {
Log::info('[FindMainNewsFileTask:findViaLLM] Начало LLM определения', [ Log::channel('ai')->info('Начало LLM определения', [
'files_count' => $docFiles->count(), 'files_count' => $docFiles->count(),
]); ]);
@@ -119,7 +119,7 @@ class FindMainNewsFileTask
// Собираем фрагменты текста из каждого файла // Собираем фрагменты текста из каждого файла
$fragments = []; $fragments = [];
foreach ($docFiles as $file) { foreach ($docFiles as $file) {
Log::info('[FindMainNewsFileTask:findViaLLM] Извлечение фрагмента', [ Log::channel('ai')->info('Извлечение фрагмента', [
'file' => $file->getClientOriginalName(), 'file' => $file->getClientOriginalName(),
]); ]);
@@ -131,11 +131,11 @@ class FindMainNewsFileTask
} }
// Отправляем запрос к LLM // Отправляем запрос к LLM
Log::info('[FindMainNewsFileTask:findViaLLM] Отправка запроса к LLM'); Log::channel('ai')->info('Отправка запроса к LLM');
$llmResponse = app(CallAiServiceForFileSelectionTask::class)->run($fragments); $llmResponse = app(CallAiServiceForFileSelectionTask::class)->run($fragments);
if ($llmResponse && isset($llmResponse['main_file'])) { if ($llmResponse && isset($llmResponse['main_file'])) {
Log::info('[FindMainNewsFileTask:findViaLLM] LLM вернула результат', [ Log::channel('ai')->info('LLM вернула результат', [
'main_file' => $llmResponse['main_file'], 'main_file' => $llmResponse['main_file'],
]); ]);
@@ -147,7 +147,7 @@ class FindMainNewsFileTask
} }
} }
Log::warning('[FindMainNewsFileTask:findViaLLM] LLM не помогла, используем первый файл'); Log::channel('ai')->warning('LLM не помогла, используем первый файл');
// Fallback — первый файл // Fallback — первый файл
return $docFiles->first(); return $docFiles->first();
@@ -14,7 +14,7 @@ class ParseDocxTask
{ {
// 1. Проверяем, существует ли физически файл по этому пути // 1. Проверяем, существует ли физически файл по этому пути
if (!file_exists($filePath)) { if (!file_exists($filePath)) {
Log::error("[ParseDocxTask] Файл не найден по пути: {$filePath}"); Log::channel('ai')->error('Файл не найден по пути: {$filePath}");
return "ОШИБКА ДЕБАГА: Файл не найден на сервере."; return "ОШИБКА ДЕБАГА: Файл не найден на сервере.";
} }
@@ -26,7 +26,7 @@ class ParseDocxTask
// Если не true, значит произошла ошибка // Если не true, значит произошла ошибка
if ($res !== true) { if ($res !== true) {
$errorMsg = $this->getZipError($res); $errorMsg = $this->getZipError($res);
Log::error("[ParseDocxTask] Ошибка ZipArchive: {$errorMsg} | Путь: {$filePath}"); Log::channel('ai')->error('Ошибка ZipArchive: {$errorMsg} | Путь: {$filePath}");
// Возвращаем текст ошибки прямо на фронт, чтобы сразу увидеть причину! // Возвращаем текст ошибки прямо на фронт, чтобы сразу увидеть причину!
return "ОШИБКА ДЕБАГА ZipArchive: {$errorMsg}"; return "ОШИБКА ДЕБАГА ZipArchive: {$errorMsg}";
@@ -39,7 +39,7 @@ class ParseDocxTask
$zip->close(); $zip->close();
if ($xmlString === false) { if ($xmlString === false) {
Log::error("[ParseDocxTask] Файл word/document.xml не найден внутри архива. Возможно, это не настоящий DOCX."); Log::channel('ai')->error('Файл word/document.xml не найден внутри архива. Возможно, это не настоящий DOCX.");
return "ОШИБКА ДЕБАГА: Внутри архива нет word/document.xml"; return "ОШИБКА ДЕБАГА: Внутри архива нет word/document.xml";
} }
@@ -49,7 +49,7 @@ class ParseDocxTask
$text = strip_tags($text); $text = strip_tags($text);
return trim($text); return trim($text);
} catch (\Exception $e) { } catch (\Exception $e) {
Log::error("[ParseDocxTask] Ошибка очистки тегов: " . $e->getMessage()); Log::channel('ai')->error('Ошибка очистки тегов: " . $e->getMessage());
return "ОШИБКА ДЕБАГА при очистке текста."; return "ОШИБКА ДЕБАГА при очистке текста.";
} }
} }