- Added comprehensive AI Assistant system (aiassist/ directory): * Vector search and embedding capabilities * Typebot proxy integration * Elastic search functionality * Message classification and chat history * MCP proxy for external integrations - Implemented Court Status API (GetCourtStatus.php): * Real-time court document status checking * Integration with external court systems * Comprehensive error handling and logging - Enhanced S3 integration: * Improved file backup system with metadata * Batch processing capabilities * Enhanced error logging and recovery * Copy operations with URL fixing - Added Telegram contact creation API - Improved error logging across all modules - Enhanced callback system for AI responses - Extensive backup file storage with timestamps - Updated documentation and README files - File storage improvements: * Thousands of backup files with proper metadata * Fix operations for broken file references * Project-specific backup and recovery systems * Comprehensive file integrity checking Total: 26,461+ files added/modified including AWS SDK, vendor dependencies, and extensive backup system.
982 lines
46 KiB
PHP
982 lines
46 KiB
PHP
<?php
|
||
error_reporting(E_ALL);
|
||
ini_set('display_errors', 1);
|
||
ini_set('display_startup_errors', 1);
|
||
ini_set('log_errors', 1);
|
||
ini_set('error_log', 'logs/php_errors.log');
|
||
|
||
// Подключаем модули
|
||
require_once 'aiassist/elastic.php'; // Файл с функциями для работы с ElasticSearch
|
||
|
||
// Настройки OpenAI API и модерации
|
||
const OPENAI_API_KEY = 'sk-GS24OxHQYfq8ErW5CRLoN5F1CfJPxNsY';
|
||
const OPENAI_ASSISTANT_API = 'https://api.proxyapi.ru/openai/v1/assistants';
|
||
const OPENAI_FILES_API = 'https://api.proxyapi.ru/openai/v1/files';
|
||
const OPENAI_THREADS_API = 'https://api.proxyapi.ru/openai/v1/threads';
|
||
const OPENAI_VECTOR_STORES_API = 'https://api.proxyapi.ru/openai/v1/vector_stores';
|
||
const OPENAI_VISION_API = 'https://api.proxyapi.ru/openai/v1/chat/completions'; // для описания изображений
|
||
// URL для NSFW-модерации
|
||
const NSFW_MODERATION_API = 'https://api.proxyapi.ru/v1/moderation/nsfw';
|
||
|
||
const LOG_FILE = 'logs/scriptass13112.log';
|
||
|
||
// ID и имя ассистента
|
||
const ASSISTANT_ID = 'asst_suGt51aoepXUkJiC0t3vobeG';
|
||
const ASSISTANT_NAME = 'Clientright';
|
||
|
||
// Для корректной обработки кириллицы
|
||
setlocale(LC_ALL, 'ru_RU.UTF-8');
|
||
|
||
// Подключение к БД (Vtiger CRM)
|
||
$dsn = 'mysql:host=localhost;port=3306;dbname=ci20465_72new;charset=utf8mb4';
|
||
$user = 'ci20465_72new';
|
||
$password = 'EcY979Rn';
|
||
|
||
|
||
|
||
try {
|
||
$pdo = new PDO($dsn, $user, $password, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
|
||
} catch (PDOException $e) {
|
||
logMessage("Ошибка подключения к БД: " . $e->getMessage());
|
||
die("Ошибка подключения к БД");
|
||
}
|
||
|
||
function logMessage($message) {
|
||
if (!is_dir('logs')) {
|
||
mkdir('logs', 0777, true);
|
||
}
|
||
file_put_contents(LOG_FILE, date('Y-m-d H:i:s') . " - " . $message . "\n", FILE_APPEND | LOCK_EX);
|
||
}
|
||
|
||
function normalizeFilename($filename) {
|
||
$filename = iconv('UTF-8', 'UTF-8//IGNORE', $filename);
|
||
return preg_replace('/[^\w\.]+/u', '_', $filename);
|
||
}
|
||
|
||
/* ===================== Основной скрипт ===================== */
|
||
|
||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||
// Получаем ID из $_POST или JSON-тела запроса
|
||
$id = $_POST['id'] ?? null;
|
||
if (!$id) {
|
||
$input = json_decode(file_get_contents('php://input'), true);
|
||
$id = $input['id'] ?? null;
|
||
}
|
||
if (!$id || !is_numeric($id)) {
|
||
logMessage("Ошибка: Некорректный ID.");
|
||
die(json_encode(["status" => "error", "message" => "Некорректный ID."], JSON_UNESCAPED_UNICODE));
|
||
}
|
||
$GLOBALS['caseId'] = $id;
|
||
// Извлекаем документы из CRM через БД
|
||
$documents = fetchDocumentData($pdo, $id);
|
||
if (empty($documents)) {
|
||
logMessage("Документы не найдены для ID: $id");
|
||
die("Документы не найдены для ID: $id");
|
||
}
|
||
logMessage("Документы получены из БД: " . json_encode($documents, JSON_UNESCAPED_UNICODE));
|
||
|
||
// Создаём массив строковых путей (извлекаем только 'filepath')
|
||
$filePathList = array_map(function($doc) {
|
||
return $doc['filepath'];
|
||
}, $documents);
|
||
|
||
// --- Проверка наличия ранее сохранённого анализа ---
|
||
|
||
/* $previousAnalysis = checkPreviousAnalysis($id, $documents);
|
||
if ($previousAnalysis) {
|
||
logMessage("✅ Используем сохранённый анализ из ElasticSearch.");
|
||
sendAnalysisToCRM($id, $previousAnalysis);
|
||
}
|
||
*/
|
||
|
||
$previousAnalysis = checkPreviousAnalysis($id, $filePathList);
|
||
logMessage("DEBUG: Значение предыдущего анализа: " . print_r($previousAnalysis, true));
|
||
if ($previousAnalysis) {
|
||
logMessage("Найден сохранённый анализ, отправляем его в CRM.");
|
||
sendAnalysisToCRM($id, $previousAnalysis);
|
||
exit;
|
||
} else {
|
||
logMessage("Сохранённый анализ не найден, продолжаем обработку.");
|
||
}
|
||
// --- Конец проверки ранее сохранённого анализа ---
|
||
|
||
|
||
|
||
// Подготовка документов и загрузка файлов
|
||
$uploadResult = createVectorStoreAndUploadFiles($filePathList);
|
||
if (!$uploadResult) {
|
||
logMessage("Ошибка создания Vector Store или загрузки файлов");
|
||
die("Ошибка создания Vector Store или загрузки файлов");
|
||
}
|
||
$vectorStoreId = $uploadResult['vectorStoreId'];
|
||
$uploadedFileIds = $uploadResult['fileIds'];
|
||
|
||
if (!updateAssistantWithVectorStore($vectorStoreId)) {
|
||
logMessage("Ошибка обновления ассистента с Vector Store");
|
||
die("Ошибка обновления ассистента");
|
||
}
|
||
|
||
$combinedContent = analyzeDocuments($documents, $uploadedFileIds);
|
||
logMessage("Собранный контент для анализа:\n" . $combinedContent);
|
||
if (empty($combinedContent)) {
|
||
logMessage("Ошибка: анализ документов не вернул результатов");
|
||
die("Ошибка: анализ документов не вернул результатов");
|
||
}
|
||
|
||
$fileIdCombined = implode(',', array_values($uploadedFileIds));
|
||
logMessage("Объединённый список идентификаторов файлов: " . $fileIdCombined);
|
||
|
||
// $threadId = createThread();
|
||
$threadId = createNewThread();
|
||
if (!$threadId) {
|
||
logMessage("Ошибка создания треда");
|
||
die("Ошибка создания треда");
|
||
}
|
||
|
||
// Запуск анализа документов через ассистента (stream)
|
||
$analysis = analyzeDocumentWithAssistantStream($threadId, ASSISTANT_ID, $fileIdCombined, $combinedContent);
|
||
if (!$analysis) {
|
||
logMessage("❌ Ошибка анализа совокупного запроса");
|
||
die("Ошибка анализа совокупного запроса");
|
||
}
|
||
|
||
// Сохранение результата анализа в ElasticSearch
|
||
$saveResult = saveAnalysisToElasticSearch($id, $analysis, $filePathList);
|
||
if ($saveResult) {
|
||
logMessage("✅ Анализ успешно сохранён в ElasticSearch.");
|
||
} else {
|
||
logMessage("❌ Ошибка при сохранении анализа в ElasticSearch.");
|
||
}
|
||
|
||
// Отправка результата в CRM через функцию sendAnalysisToCRM()
|
||
sendAnalysisToCRM($id, $analysis);
|
||
|
||
// Если функция sendAnalysisToCRM() корректно завершила работу, выполнение дальше не продолжается.
|
||
logMessage("Обработка всех документов завершена.");
|
||
} else {
|
||
logMessage("Ошибка: запрос должен быть POST");
|
||
die("Ошибка: запрос должен быть POST");
|
||
}
|
||
|
||
/* ===================== Функция отправки результата в CRM ===================== */
|
||
|
||
/*function sendAnalysisToCRM($caseId, $analysis) {
|
||
// Формируем финальный ответ
|
||
$final_output = [
|
||
"status" => $analysis['status'] ?? 'error',
|
||
"content" => $analysis['content'] ?? 'Анализ не выполнен',
|
||
"moderationVerdict" => $analysis['moderationVerdict'] ?? 'Не определен'
|
||
];
|
||
|
||
// Логируем финальные данные перед отправкой
|
||
logMessage("DEBUG: Финальный массив данных: " . print_r($final_output, true));
|
||
|
||
// Кодируем в JSON
|
||
$json_output = json_encode($final_output, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
||
|
||
// Проверка на ошибки кодирования
|
||
if ($json_output === false) {
|
||
$error = json_last_error_msg();
|
||
logMessage("ERROR: Ошибка кодирования JSON: $error");
|
||
die(json_encode(["status" => "error", "message" => "Ошибка формирования ответа"]));
|
||
}
|
||
|
||
// Логируем итоговый JSON
|
||
logMessage("DEBUG: Итоговый JSON для CRM: " . $json_output);
|
||
|
||
// Отправляем JSON-ответ
|
||
echo $json_output;
|
||
|
||
// Логируем завершение
|
||
logMessage("Обработка завершена. Ответ успешно отправлен в CRM");
|
||
exit;
|
||
}
|
||
|
||
*/
|
||
|
||
|
||
function sendAnalysisToCRM($caseId, $analysis) {
|
||
if (!isset($analysis['content'])) {
|
||
$analysis['content'] = $analysis['анализ_gpt'] ?? 'Анализ не выполнен';
|
||
}
|
||
if (!isset($analysis['moderationVerdict'])) {
|
||
$analysis['moderationVerdict'] = $analysis['вывод_gpt'] ?? 'Не определен';
|
||
}
|
||
if (!isset($analysis['status'])) {
|
||
$analysis['status'] = 'complete';
|
||
}
|
||
|
||
$final_output = [
|
||
"status" => $analysis['status'],
|
||
"content" => $analysis['content'],
|
||
"moderationVerdict" => $analysis['moderationVerdict']
|
||
];
|
||
|
||
// Кодируем в JSON
|
||
$json_output = json_encode($final_output, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
||
|
||
// Проверка на ошибки кодирования
|
||
if ($json_output === false) {
|
||
$error = json_last_error_msg();
|
||
logMessage("ERROR: Ошибка кодирования JSON: $error");
|
||
die(json_encode(["status" => "error", "message" => "Ошибка формирования ответа"]));
|
||
}
|
||
|
||
// Логируем итоговый JSON
|
||
logMessage("DEBUG: Итоговый JSON для CRM: " . $json_output);
|
||
|
||
echo json_encode($final_output, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
||
|
||
// Логируем завершение
|
||
logMessage("Обработка завершена. Ответ успешно отправлен в CRM");
|
||
exit;
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
/* ===================== Функции для работы с CRM и Vector Store ===================== */
|
||
|
||
/**
|
||
* Проверяет, является ли файл судебным решением, по ключевым словам в названии или содержимом.
|
||
*
|
||
* @param string $filename Название файла.
|
||
* @param string $content Извлечённый текст документа.
|
||
* @return bool
|
||
*/
|
||
function isCourtDecision($filename, $content) {
|
||
$keywords = ['решение суда', 'решение', 'судебное решение', 'постановление', 'определение', 'приказ'];
|
||
foreach ($keywords as $keyword) {
|
||
if (stripos($filename, $keyword) !== false || stripos($content, $keyword) !== false) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
|
||
function fetchDocumentData($pdo, $id) {
|
||
logMessage("Получение данных документа из CRM по ID: $id");
|
||
$sql = "
|
||
SELECT
|
||
n.title,
|
||
CASE
|
||
WHEN a.storedname IS NOT NULL THEN CONCAT(a.path, a.attachmentsid, '_', a.storedname)
|
||
ELSE CONCAT(a.path, a.attachmentsid, '_', a.name)
|
||
END AS filepath, f.foldername AS folder_name, f.folderid AS folder_id
|
||
FROM vtiger_senotesrel r
|
||
LEFT JOIN vtiger_notes n ON n.notesid = r.notesid
|
||
LEFT JOIN vtiger_crmentity e ON e.crmid = r.notesid
|
||
LEFT JOIN vtiger_notescf ncf ON ncf.notesid = r.notesid
|
||
LEFT JOIN vtiger_seattachmentsrel r2 ON r2.crmid = r.notesid
|
||
LEFT JOIN vtiger_attachments a ON a.attachmentsid = r2.attachmentsid
|
||
LEFT JOIN vtiger_attachmentsfolder f ON f.folderid = n.folderid
|
||
WHERE r.crmid = ? AND e.deleted = 0
|
||
AND (a.type = 'application/pdf'
|
||
OR a.type = 'application/octet-stream')
|
||
";
|
||
|
||
try {
|
||
$stmt = $pdo->prepare($sql);
|
||
$stmt->execute([$id]);
|
||
$documents = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||
logMessage("Документы получены из CRM: " . json_encode($documents, JSON_UNESCAPED_UNICODE));
|
||
return $documents;
|
||
} catch (PDOException $e) {
|
||
logMessage("Ошибка при выполнении запроса к CRM: " . $e->getMessage());
|
||
return [];
|
||
}
|
||
}
|
||
|
||
function createVectorStoreAndUploadFiles($filePaths) {
|
||
logMessage("Создание Vector Store и загрузка файлов...");
|
||
$vectorStoreId = createVectorStore();
|
||
if (!$vectorStoreId) return null;
|
||
$uploadedFiles = [];
|
||
foreach ($filePaths as $filePath) {
|
||
logMessage("Загрузка файла: $filePath");
|
||
if (!file_exists($filePath)) {
|
||
logMessage("Ошибка: Файл не существует: $filePath");
|
||
continue;
|
||
}
|
||
$fileId = uploadFileToOpenAI($filePath);
|
||
if (!$fileId) {
|
||
logMessage("Ошибка загрузки файла: $filePath");
|
||
continue;
|
||
}
|
||
if (!addFileToVectorStore($vectorStoreId, $fileId)) {
|
||
logMessage("Ошибка добавления файла в Vector Store: $filePath");
|
||
} else {
|
||
logMessage("Файл успешно добавлен в Vector Store: $filePath");
|
||
$uploadedFiles[$filePath] = $fileId;
|
||
}
|
||
}
|
||
return ['vectorStoreId' => $vectorStoreId, 'fileIds' => $uploadedFiles];
|
||
}
|
||
|
||
function createVectorStore() {
|
||
$curl = curl_init();
|
||
$payload = json_encode(['name' => 'Vector Store']);
|
||
logMessage("Создание Vector Store. Отправляем payload: " . $payload);
|
||
curl_setopt_array($curl, [
|
||
CURLOPT_URL => OPENAI_VECTOR_STORES_API,
|
||
CURLOPT_RETURNTRANSFER => true,
|
||
CURLOPT_POST => true,
|
||
CURLOPT_POSTFIELDS => $payload,
|
||
CURLOPT_HTTPHEADER => [
|
||
'Content-Type: application/json',
|
||
'Authorization: Bearer ' . OPENAI_API_KEY,
|
||
'OpenAI-Beta: assistants=v2'
|
||
]
|
||
]);
|
||
$response = curl_exec($curl);
|
||
$httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
||
$curlError = curl_error($curl);
|
||
curl_close($curl);
|
||
logMessage("Ответ OpenAI (создание Vector Store): HTTP $httpCode - " . $response);
|
||
if ($curlError) {
|
||
logMessage("Ошибка cURL при создании Vector Store: " . $curlError);
|
||
return null;
|
||
}
|
||
$decoded = json_decode($response, true);
|
||
if ($httpCode !== 200 || !isset($decoded['id'])) {
|
||
logMessage("Ошибка при создании Vector Store: " . json_encode($decoded, JSON_UNESCAPED_UNICODE));
|
||
return null;
|
||
}
|
||
return $decoded['id'];
|
||
}
|
||
|
||
function uploadFileToOpenAI($filePath) {
|
||
logMessage("Загрузка файла в OpenAI: $filePath");
|
||
$curl = curl_init();
|
||
curl_setopt_array($curl, [
|
||
CURLOPT_URL => OPENAI_FILES_API,
|
||
CURLOPT_RETURNTRANSFER => true,
|
||
CURLOPT_POST => true,
|
||
CURLOPT_POSTFIELDS => [
|
||
'file' => new CURLFile($filePath),
|
||
'purpose' => 'assistants'
|
||
],
|
||
CURLOPT_HTTPHEADER => [
|
||
'Authorization: Bearer ' . OPENAI_API_KEY,
|
||
'OpenAI-Beta: assistants=v2'
|
||
]
|
||
]);
|
||
$response = curl_exec($curl);
|
||
$httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
||
$curlError = curl_error($curl);
|
||
curl_close($curl);
|
||
logMessage("Ответ OpenAI (загрузка файла): HTTP $httpCode - " . $response);
|
||
if ($curlError) {
|
||
logMessage("Ошибка cURL при загрузке файла: " . $curlError);
|
||
return null;
|
||
}
|
||
$decoded = json_decode($response, true);
|
||
if ($httpCode !== 200 || !isset($decoded['id'])) {
|
||
logMessage("Ошибка при загрузке файла: " . json_encode($decoded, JSON_UNESCAPED_UNICODE));
|
||
return null;
|
||
}
|
||
return $decoded['id'];
|
||
}
|
||
|
||
function addFileToVectorStore($vectorStoreId, $fileId) {
|
||
$curl = curl_init();
|
||
$payload = json_encode(['file_id' => $fileId]);
|
||
logMessage("Добавление файла в Vector Store. Payload: " . $payload);
|
||
curl_setopt_array($curl, [
|
||
CURLOPT_URL => OPENAI_VECTOR_STORES_API . "/$vectorStoreId/files",
|
||
CURLOPT_RETURNTRANSFER => true,
|
||
CURLOPT_POST => true,
|
||
CURLOPT_POSTFIELDS => $payload,
|
||
CURLOPT_HTTPHEADER => [
|
||
'Content-Type: application/json',
|
||
'Authorization: Bearer ' . OPENAI_API_KEY,
|
||
'OpenAI-Beta: assistants=v2'
|
||
]
|
||
]);
|
||
$response = curl_exec($curl);
|
||
$httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
||
$curlError = curl_error($curl);
|
||
curl_close($curl);
|
||
logMessage("Ответ OpenAI (добавление файла): HTTP $httpCode - " . $response);
|
||
if ($curlError) {
|
||
logMessage("Ошибка cURL при добавлении файла в Vector Store: " . $curlError);
|
||
return false;
|
||
}
|
||
$decoded = json_decode($response, true);
|
||
if ($httpCode !== 200 || !isset($decoded['id'])) {
|
||
logMessage("Ошибка добавления файла: " . json_encode($decoded, JSON_UNESCAPED_UNICODE));
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
function updateAssistantWithVectorStore($vectorStoreId) {
|
||
$data = [
|
||
'tool_resources' => [
|
||
'file_search' => [
|
||
'vector_store_ids' => [$vectorStoreId]
|
||
]
|
||
]
|
||
];
|
||
$curl = curl_init();
|
||
$payload = json_encode($data);
|
||
logMessage("Обновление ассистента. Payload: " . $payload);
|
||
curl_setopt_array($curl, [
|
||
CURLOPT_URL => OPENAI_ASSISTANT_API . "/" . ASSISTANT_ID,
|
||
CURLOPT_RETURNTRANSFER => true,
|
||
CURLOPT_CUSTOMREQUEST => 'POST',
|
||
CURLOPT_POSTFIELDS => $payload,
|
||
CURLOPT_HTTPHEADER => [
|
||
'Content-Type: application/json',
|
||
'Authorization: Bearer ' . OPENAI_API_KEY,
|
||
'OpenAI-Beta: assistants=v2'
|
||
]
|
||
]);
|
||
$response = curl_exec($curl);
|
||
$httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
||
$curlError = curl_error($curl);
|
||
curl_close($curl);
|
||
logMessage("Ответ OpenAI (обновление ассистента): HTTP $httpCode - " . $response);
|
||
if ($curlError) {
|
||
logMessage("Ошибка обновления ассистента: " . $curlError);
|
||
return false;
|
||
}
|
||
$decoded = json_decode($response, true);
|
||
if ($httpCode !== 200 || !isset($decoded['id'])) {
|
||
logMessage("Ошибка обновления ассистента: " . json_encode($decoded, JSON_UNESCAPED_UNICODE));
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
/* Новая функция для извлечения контента документа */
|
||
function extractDocumentContent($filePath) {
|
||
// 1. Попытка извлечь текст через pdftotext
|
||
$text = extractText($filePath);
|
||
if (mb_strlen($text, 'UTF-8') >= 200) {
|
||
logMessage("pdftotext дал достаточное количество символов.");
|
||
return ['mode' => 'pdf', 'content' => $text];
|
||
} else {
|
||
logMessage("pdftotext дал недостаточное количество символов. Переходим к обработке через изображения.");
|
||
// 2. Конвертация PDF в изображения (JPG)
|
||
$outputDir = sys_get_temp_dir() . '/pdf_images_' . md5($filePath);
|
||
$images = convertPdfToImages($filePath, $outputDir);
|
||
if (empty($images)) {
|
||
logMessage("Ошибка: не удалось конвертировать PDF в изображения для $filePath");
|
||
return ['mode' => 'error', 'content' => ''];
|
||
}
|
||
|
||
// 3. Проверка каждого изображения на NSFW
|
||
$allSafe = true;
|
||
foreach ($images as $image) {
|
||
$classification = classifyImage($image);
|
||
$absImagePath = realpath($image);
|
||
$unsafeProbability = isset($classification[$absImagePath]) ? ($classification[$absImagePath]['unsafe'] ?? 0) : 0;
|
||
logMessage("DEBUG: Для изображения '$absImagePath' получено unsafeProbability = " . $unsafeProbability);
|
||
if ($unsafeProbability > 0.8) {
|
||
logMessage("DEBUG: Изображение '$absImagePath' не прошло NSFW проверку.");
|
||
$allSafe = false;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (!$allSafe) {
|
||
return ['mode' => 'nsfw', 'content' => 'Файл содержит трешконтент.'];
|
||
} else {
|
||
// 4. Если все изображения безопасны, запускаем OCR для извлечения текста
|
||
$combinedOcrText = "";
|
||
foreach ($images as $image) {
|
||
$ocrText = doOCR($image);
|
||
if (!empty($ocrText)) {
|
||
logMessage("DEBUG: OCR успешно извлечёк текст для изображения: " . $image);
|
||
$combinedOcrText .= $ocrText . "\n";
|
||
} else {
|
||
logMessage("DEBUG: OCR не смог извлечь текст для изображения: " . $image);
|
||
}
|
||
}
|
||
if (empty(trim($combinedOcrText))) {
|
||
logMessage("Ошибка: Не удалось извлечь текст посредством OCR для $filePath");
|
||
return ['mode' => 'error', 'content' => ''];
|
||
}
|
||
return ['mode' => 'ocr', 'content' => trim($combinedOcrText)];
|
||
}
|
||
}
|
||
}
|
||
|
||
/* СТАРАЯ Функция analyzeDocuments теперь использует extractDocumentContent() */
|
||
/*function analyzeDocuments($documents, $uploadedFileIds) {
|
||
$combinedMessages = [];
|
||
foreach ($documents as $doc) {
|
||
if (empty($doc['filepath']) || strpos($doc['filepath'], '_') === 0) {
|
||
logMessage("Неверный путь: " . json_encode($doc, JSON_UNESCAPED_UNICODE));
|
||
continue;
|
||
}
|
||
$normalizedPath = normalizeFilename($doc['filepath']);
|
||
$messageBlock = "Документ: " . $doc['title'] . "\n";
|
||
|
||
$extraction = extractDocumentContent($doc['filepath']);
|
||
if ($extraction['mode'] === 'pdf') {
|
||
logMessage("DEBUG: Извлечение текста успешно для " . $doc['filepath'] . ". Передаём PDF файл.");
|
||
$fileId = $uploadedFileIds[$doc['filepath']] ?? '';
|
||
$messageBlock .= "Тип: PDF (извлечение текста успешно, PDF передан для анализа).\nFileID: " . $fileId . "\n";
|
||
} elseif ($extraction['mode'] === 'ocr') {
|
||
logMessage("DEBUG: Извлечение текста посредством OCR для " . $doc['filepath']);
|
||
$messageBlock .= "OCR извлёк текст:\n" . $extraction['content'] . "\n";
|
||
} elseif ($extraction['mode'] === 'nsfw') {
|
||
logMessage("DEBUG: Файл " . $doc['filepath'] . " не прошёл NSFW проверку.");
|
||
$messageBlock .= "Ошибка: Файл не прошёл цензуру. Файл: " . $normalizedPath . "\n";
|
||
} else {
|
||
$messageBlock .= "Ошибка: Не удалось извлечь текст. Файл: " . $normalizedPath . "\n";
|
||
}
|
||
$combinedMessages[] = $messageBlock;
|
||
}
|
||
return implode("\n-----------------\n", $combinedMessages);
|
||
}
|
||
*/
|
||
|
||
|
||
//НОВАЯ analyzeDocuments
|
||
function analyzeDocuments($documents, $uploadedFileIds) {
|
||
$combinedMessages = [];
|
||
foreach ($documents as $doc) {
|
||
if (empty($doc['filepath']) || strpos($doc['filepath'], '_') === 0) {
|
||
logMessage("Неверный путь: " . json_encode($doc, JSON_UNESCAPED_UNICODE));
|
||
continue;
|
||
}
|
||
$normalizedPath = normalizeFilename($doc['filepath']);
|
||
// $messageBlock = "Документ: " . $doc['title'] . "\n";
|
||
$messageBlock = "Документ: " . $doc['title'] . "папка" . $doc['folder_name'] ."\n";
|
||
$extraction = extractDocumentContent($doc['filepath']);
|
||
|
||
// Если документ содержит ключевые слова, указывающие на судебное решение
|
||
|
||
/*if (isCourtDecision($doc['title'], $extraction['content'])) {
|
||
logMessage("📌 Найдено судебное решение: " . $doc['title']);
|
||
Сохраняем судебное решение в отдельный индекс
|
||
saveCourtDecisionToElastic($GLOBALS['caseId'], $doc['title'], $extraction['content']);
|
||
$messageBlock .= "Тип: Судебное решение (сохранено отдельно).\n";
|
||
*/
|
||
// Проверяем, лежит ли документ в папке "судебные решения" или определён ли он как судебный акт
|
||
|
||
if (isset($doc['folder_name']) && mb_stripos($doc['folder_name'], 'судебные решения') !== false) {
|
||
logMessage("📌 Найдено судебное решение в папке '{$doc['folder_name']}': " . $doc['title']);
|
||
|
||
saveCourtDecisionToElastic($GLOBALS['caseId'], $doc['title'], $extraction['content']);
|
||
|
||
$messageBlock .= "Тип: Судебное решение (сохранено отдельно).\n";
|
||
} else {
|
||
// Остальная обработка документа
|
||
if ($extraction['mode'] === 'pdf') {
|
||
logMessage("DEBUG: Извлечение текста успешно для " . $doc['filepath'] . ". Передаём PDF файл.");
|
||
$fileId = $uploadedFileIds[$doc['filepath']] ?? '';
|
||
$messageBlock .= "Тип: PDF (извлечение текста успешно, PDF передан для анализа).\nFileID: " . $fileId . "\n";
|
||
} elseif ($extraction['mode'] === 'ocr') {
|
||
logMessage("DEBUG: Извлечение текста посредством OCR для " . $doc['filepath']);
|
||
$messageBlock .= "OCR извлёк текст:\n" . $extraction['content'] . "\n";
|
||
} elseif ($extraction['mode'] === 'nsfw') {
|
||
logMessage("DEBUG: Файл " . $doc['filepath'] . " не прошёл NSFW проверку.");
|
||
$messageBlock .= "Ошибка: Файл не прошёл цензуру. Файл: " . $normalizedPath . "\n";
|
||
} else {
|
||
$messageBlock .= "Ошибка: Не удалось извлечь текст. Файл: " . $normalizedPath . "\n";
|
||
}
|
||
}
|
||
|
||
$combinedMessages[] = $messageBlock;
|
||
}
|
||
return implode("\n-----------------\n", $combinedMessages);
|
||
}
|
||
|
||
/*
|
||
function createThread() {
|
||
$curl = curl_init();
|
||
logMessage("Создание треда для анализа документа.");
|
||
curl_setopt_array($curl, [
|
||
CURLOPT_URL => OPENAI_THREADS_API,
|
||
CURLOPT_RETURNTRANSFER => true,
|
||
CURLOPT_POST => true,
|
||
CURLOPT_HTTPHEADER => [
|
||
'Authorization: Bearer ' . OPENAI_API_KEY,
|
||
'OpenAI-Beta: assistants=v2'
|
||
]
|
||
]);
|
||
$response = curl_exec($curl);
|
||
$decoded = json_decode($response, true);
|
||
$threadId = $decoded['id'] ?? null;
|
||
logMessage("Создан тред с id: " . ($threadId ?: "не удалось создать") . ". Ответ: " . $response);
|
||
return $threadId;
|
||
}
|
||
*/
|
||
|
||
function createNewThread() {
|
||
$curl = curl_init();
|
||
curl_setopt_array($curl, [
|
||
CURLOPT_URL => OPENAI_THREADS_API,
|
||
CURLOPT_RETURNTRANSFER => true,
|
||
CURLOPT_POST => true,
|
||
CURLOPT_HTTPHEADER => [
|
||
'Authorization: Bearer ' . OPENAI_API_KEY,
|
||
'OpenAI-Beta: assistants=v2'
|
||
]
|
||
]);
|
||
$response = curl_exec($curl);
|
||
$decoded = json_decode($response, true);
|
||
curl_close($curl);
|
||
|
||
if (isset($decoded['id'])) {
|
||
logMessage("🔄 Создан новый thread_id: " . $decoded['id']);
|
||
return $decoded['id'];
|
||
} else {
|
||
logMessage("❌ Ошибка создания нового треда: " . json_encode($decoded));
|
||
return null;
|
||
}
|
||
}
|
||
|
||
|
||
|
||
function extractText($filePath) {
|
||
$extension = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
|
||
if ($extension !== 'pdf') {
|
||
return '';
|
||
}
|
||
$outputFile = tempnam(sys_get_temp_dir(), 'txt_') . '.txt';
|
||
$command = "pdftotext " . escapeshellarg($filePath) . " " . escapeshellarg($outputFile);
|
||
logMessage("Выполняем команду для извлечения текста: " . $command);
|
||
exec($command, $output, $returnVar);
|
||
if ($returnVar !== 0) {
|
||
logMessage("Ошибка pdftotext: " . implode("\n", $output));
|
||
return '';
|
||
}
|
||
if (!file_exists($outputFile)) {
|
||
logMessage("Файл pdftotext не создан: $filePath");
|
||
return '';
|
||
}
|
||
$text = file_get_contents($outputFile);
|
||
unlink($outputFile);
|
||
$cleanText = trim(preg_replace('/\pC+/u', '', $text));
|
||
if (mb_strlen($cleanText, 'UTF-8') < 200) {
|
||
logMessage("Извлечённый текст для $filePath слишком короткий или не содержит полезной информации. Запуск OCR.");
|
||
return '';
|
||
}
|
||
$snippet = mb_substr($cleanText, 0, 500, 'UTF-8');
|
||
logMessage("Извлечённый текст для $filePath (первые 500 символов): " . $snippet);
|
||
return $cleanText;
|
||
}
|
||
|
||
// Распознавание через OCR
|
||
function doOCR($filePath) {
|
||
logMessage("Запуск OCR для файла: $filePath");
|
||
$outputFile = tempnam(sys_get_temp_dir(), 'ocr_') . '.txt';
|
||
// Изменённая команда: добавлены --psm 6 и --oem 1
|
||
$command = "tesseract " . escapeshellarg($filePath) . " " . escapeshellarg($outputFile) . " -l rus --psm 6 --oem 1";
|
||
logMessage("Выполняем команду OCR: " . $command);
|
||
exec($command, $output, $returnVar);
|
||
if ($returnVar !== 0) {
|
||
logMessage("Ошибка Tesseract: " . implode("\n", $output));
|
||
return '';
|
||
}
|
||
if (!file_exists($outputFile . ".txt")) {
|
||
logMessage("Файл OCR не создан: $filePath");
|
||
return '';
|
||
}
|
||
$text = file_get_contents($outputFile . ".txt");
|
||
if (empty($text)) {
|
||
logMessage("DEBUG: Tesseract вернул пустой результат для $filePath");
|
||
}
|
||
unlink($outputFile . ".txt");
|
||
return $text;
|
||
}
|
||
|
||
|
||
|
||
function describeImageWithVision($filePath) {
|
||
logMessage("Запуск описания изображения через Vision для файла: $filePath");
|
||
$imageData = base64_encode(file_get_contents($filePath));
|
||
$data = [
|
||
"model" => "gpt-4-vision-preview",
|
||
"messages" => [
|
||
[
|
||
"role" => "user",
|
||
"content" => [
|
||
[
|
||
"type" => "text",
|
||
"text" => "Опиши это изображение подробно. Если это документ, прочитай и опиши его содержимое."
|
||
],
|
||
[
|
||
"type" => "image_url",
|
||
"image_url" => [
|
||
"url" => "data:image/jpeg;base64,$imageData"
|
||
]
|
||
]
|
||
]
|
||
]
|
||
],
|
||
"max_tokens" => 500
|
||
];
|
||
$curl = curl_init();
|
||
logMessage("Отправляем запрос на описание изображения. Payload: " . json_encode($data));
|
||
curl_setopt_array($curl, [
|
||
CURLOPT_URL => OPENAI_VISION_API,
|
||
CURLOPT_RETURNTRANSFER => true,
|
||
CURLOPT_POST => true,
|
||
CURLOPT_POSTFIELDS => json_encode($data),
|
||
CURLOPT_HTTPHEADER => [
|
||
'Content-Type: application/json',
|
||
'Authorization: Bearer ' . OPENAI_API_KEY
|
||
]
|
||
]);
|
||
$response = curl_exec($curl);
|
||
$httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
||
$curlError = curl_error($curl);
|
||
curl_close($curl);
|
||
logMessage("Ответ Vision (описание): HTTP $httpCode - " . $response);
|
||
if ($curlError) {
|
||
logMessage("Ошибка cURL в описании изображения: " . $curlError);
|
||
return '';
|
||
}
|
||
$decoded = json_decode($response, true);
|
||
if (isset($decoded['choices'][0]['message']['content'])) {
|
||
$desc = $decoded['choices'][0]['message']['content'];
|
||
if (empty($desc)) {
|
||
logMessage("DEBUG: Описание изображения пустое для $filePath");
|
||
}
|
||
return $desc;
|
||
} else {
|
||
logMessage("Ошибка при получении описания изображения: " . json_encode($decoded));
|
||
return '';
|
||
}
|
||
}
|
||
|
||
function checkNSFWWithVision($filePath) {
|
||
logMessage("NSFW-проверка через URL " . NSFW_MODERATION_API . " для файла: $filePath");
|
||
$curl = curl_init();
|
||
curl_setopt_array($curl, [
|
||
CURLOPT_URL => NSFW_MODERATION_API,
|
||
CURLOPT_RETURNTRANSFER => true,
|
||
CURLOPT_POST => true,
|
||
CURLOPT_POSTFIELDS => [
|
||
'file' => new CURLFile($filePath)
|
||
],
|
||
CURLOPT_HTTPHEADER => [
|
||
'Authorization: Bearer ' . OPENAI_API_KEY
|
||
]
|
||
]);
|
||
$response = curl_exec($curl);
|
||
$httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
||
$curlError = curl_error($curl);
|
||
curl_close($curl);
|
||
logMessage("Ответ NSFW: HTTP $httpCode - " . $response);
|
||
if ($curlError) {
|
||
logMessage("Ошибка cURL при проверке NSFW: " . $curlError);
|
||
return null;
|
||
}
|
||
$decoded = json_decode($response, true);
|
||
if ($httpCode !== 200 || isset($decoded['detail'])) {
|
||
logMessage("Ошибка анализа NSFW: " . json_encode($decoded, JSON_UNESCAPED_UNICODE));
|
||
return null;
|
||
}
|
||
return $decoded['nsfw'] ?? null;
|
||
}
|
||
|
||
function classifyImage($imagePath) {
|
||
$absolutePath = realpath($imagePath);
|
||
if (!$absolutePath) {
|
||
logMessage("ERROR: Не удалось получить абсолютный путь для " . $imagePath);
|
||
return [];
|
||
}
|
||
logMessage("DEBUG: Абсолютный путь для классификации: " . $absolutePath);
|
||
$escapedPath = escapeshellarg($absolutePath);
|
||
logMessage("DEBUG: Экранированный путь для классификации: " . $escapedPath);
|
||
$command = "python3 -c \"import json; from nudenet import NudeClassifier; classifier = NudeClassifier(); print(json.dumps(classifier.classify($escapedPath)))\"";
|
||
logMessage("DEBUG: Выполнение команды: " . $command);
|
||
$output = shell_exec($command);
|
||
logMessage("DEBUG: Вывод команды: " . $output);
|
||
if ($output === null) {
|
||
logMessage("ERROR: shell_exec вернул null при выполнении NudeClassifier");
|
||
return [];
|
||
}
|
||
return json_decode(trim($output), true);
|
||
}
|
||
|
||
function convertPdfToImages($pdfPath, $outputDir) {
|
||
if (!file_exists($pdfPath)) {
|
||
logMessage("Файл не существует: $pdfPath");
|
||
return [];
|
||
}
|
||
if (!file_exists($outputDir)) {
|
||
mkdir($outputDir, 0777, true);
|
||
logMessage("Создана директория для изображений: $outputDir");
|
||
}
|
||
$imagePattern = $outputDir . '/page-%03d.jpg';
|
||
$command = "LC_ALL=en_US.UTF-8 convert -density 300 " . escapeshellarg($pdfPath) . " -quality 90 " . escapeshellarg($imagePattern);
|
||
logMessage("Выполняем команду: " . $command);
|
||
exec($command . " 2>&1", $output, $returnVar);
|
||
logMessage("DEBUG: Вывод convert: " . implode("\n", $output));
|
||
if ($returnVar !== 0) {
|
||
logMessage("Ошибка при конвертации PDF в изображения.");
|
||
return [];
|
||
}
|
||
return glob($outputDir . '/*.jpg');
|
||
}
|
||
|
||
function getKnowledgeBaseContext($filePath) {
|
||
return "Статическая информация: нормы и законы РФ, судебные прецеденты...";
|
||
}
|
||
|
||
/**
|
||
* Функция analyzeDocumentWithAssistantStream отправляет запрос к ассистенту с параметром "stream": true,
|
||
* накапливает потоковый ответ и парсит его для получения итогового сообщения.
|
||
*/
|
||
function analyzeDocumentWithAssistantStream($threadId, $assistantId, $fileId, $content) {
|
||
logMessage("Анализ документа через ассистента (stream): thread_id=$threadId, fileId=$fileId");
|
||
$userMessage = "🔹 Отвечай по шаблону:
|
||
Если в запросе присутствуют файлы (например, PDF или изображения), извлеки из них текст для анализа, даже если содержимое уже передано отдельно.
|
||
|
||
Задача:
|
||
Проанализируй загруженные документы, выполнив следующие действия:
|
||
|
||
1️⃣ Список файлов и проверка соответствия названий
|
||
|
||
Перечисли все загруженные файлы, без указания технических идентификаторов (например, строк вида «file-…») и служебных слов (например, «папкаСуд») в названии.
|
||
Сравни название файлов с их содержимым. Если имеются расхождения (например, название указывает на договор, а текст не извлечён), укажи, что имеется в виду.
|
||
Если файл содержит изображение, сначала извлеки текст (если он присутствует) и проанализируй его содержание; если текст отсутствует, опиши, что изображено.
|
||
Особое внимание удели NSFW-контенту в тексте и изображениях:
|
||
Если присутствует пометка nsfw_alert со значением true, укажи, что файл может содержать неприемлемый контент и требует ручной проверки.
|
||
2️⃣ Краткий анализ спора
|
||
|
||
Определи истца (потребителя) (укажи название или ФИО) и ответчика (компанию или ИП, на которую подана жалоба) (укажи название или ФИО).
|
||
Дополнительно: Проверь, совпадает ли лицо, отправившее сообщение (истец) с лицом, которое заключило договор (согласно содержимому файлов). Если лица не совпадают, добавь, что требуется ручная проверка и уточнение данной информации.
|
||
Опиши суть спора (что произошло и какая проблема заявлена).
|
||
Укажи основные аргументы сторон (что заявляет потребитель и какие возражения могут быть у компании).
|
||
3️⃣ Проверка на цензуру
|
||
|
||
Проверь документы на наличие ненормативной лексики и нецензурных изображений.
|
||
Анализируй изображения на предмет нецензурного контента, запрещенной символики, сцен насилия и других нарушений. Если найдены такие элементы, укажи это в отчете, а также отметь, требуется ли ручная проверка.
|
||
4️⃣ Выдача итогового вердикта
|
||
|
||
Если всё соответствует требованиям, укажи строго:
|
||
«Вердикт: Прошло модерацию.»
|
||
Важно: Если у файла не извлечён текст (то есть содержание недоступно для анализа) или выявлены несоответствия (например, отсутствие сопоставимости между названием и содержимым, либо расхождение между лицом-отправителем и лицом, заключившим договор), модерация не должна считаться пройденной – необходимо отметить, что требуется ручная проверка.
|
||
5️⃣ Характер спора
|
||
|
||
Дай краткую характеристику дела (например: «Характер спора: некачественная услуга», «Характер спора: невыполнение условий договора», «Характер спора: возврат денег» и т. д.).
|
||
6️⃣ Вероятность положительного решения спора
|
||
|
||
Укажи вероятность положительного решения спора в пользу потребителя в формате:
|
||
«Вероятность положительного решения спора: _____ %».
|
||
7️⃣ Чего не хватает/запросить у Ответчика
|
||
|
||
Перечисли, какие документы или сведения необходимо запросить у ответчика для полного анализа и проведения процедуры медиации. Формат:
|
||
«Запросить: …».
|
||
8️⃣ Чего не хватает/запросить у Истца
|
||
|
||
Укажи, каких дополнительных документов или сведений не хватает для составления более полного отчёта и определения вариантов решения спора в пользу потребителя. Формат:
|
||
«Запросить: …».
|
||
📌 Важно:
|
||
Отчёт должен быть структурированным, чётким и лаконичным. Укажи, какими нормами права РФ будет регулироваться рассмотрение данного спора. Если обнаружены проблемы (например, отсутствие извлечённого текста или несоответствие лиц по договору), укажи приоритетные файлы для ручной проверки." .
|
||
"Файлы: " . $fileId . "\n" .
|
||
"Содержимое для анализа:\n" . $content;
|
||
logMessage("Формируем текст запроса для ассистента (stream):\n" . $userMessage);
|
||
|
||
$userMessage = mb_convert_encoding($userMessage, 'UTF-8', 'auto');
|
||
$userMessage = iconv("UTF-8", "UTF-8//IGNORE//TRANSLIT", $userMessage);
|
||
|
||
$payload = [
|
||
"assistant_id" => $assistantId,
|
||
"thread" => [
|
||
"messages" => [
|
||
["role" => "user", "content" => $userMessage]
|
||
]
|
||
],
|
||
"stream" => true
|
||
];
|
||
$payloadJson = json_encode($payload, JSON_UNESCAPED_UNICODE);
|
||
logMessage("Отправляем запрос к ассистенту (stream). Payload:\n" . $payloadJson);
|
||
|
||
$finalMessage = "";
|
||
$curl = curl_init();
|
||
curl_setopt_array($curl, [
|
||
CURLOPT_URL => OPENAI_THREADS_API . "/runs",
|
||
CURLOPT_RETURNTRANSFER => false,
|
||
CURLOPT_POST => true,
|
||
CURLOPT_POSTFIELDS => $payloadJson,
|
||
CURLOPT_HTTPHEADER => [
|
||
'Content-Type: application/json',
|
||
'Authorization: Bearer ' . OPENAI_API_KEY,
|
||
'OpenAI-Beta: assistants=v2'
|
||
],
|
||
CURLOPT_WRITEFUNCTION => function($ch, $data) use (&$finalMessage) {
|
||
$finalMessage .= $data;
|
||
return strlen($data);
|
||
}
|
||
]);
|
||
|
||
curl_exec($curl);
|
||
$curlError = curl_error($curl);
|
||
if ($curlError) {
|
||
logMessage("Ошибка cURL в analyzeDocumentWithAssistantStream: " . $curlError);
|
||
curl_close($curl);
|
||
return null;
|
||
}
|
||
curl_close($curl);
|
||
|
||
logMessage("Сырой потоковый ответ (raw stream):\n" . $finalMessage);
|
||
|
||
// Парсинг ответа (ожидается SSE-формат)
|
||
$parsedMessage = "";
|
||
$lines = explode("\n", $finalMessage);
|
||
foreach ($lines as $line) {
|
||
$line = trim($line);
|
||
if (strpos($line, "data: ") === 0) {
|
||
$dataPart = substr($line, 6);
|
||
if ($dataPart === "[DONE]") {
|
||
break;
|
||
}
|
||
$json = json_decode($dataPart, true);
|
||
logMessage("DEBUG: Распарсенный фрагмент: " . print_r($json, true));
|
||
if (is_array($json) && isset($json['delta']['content'])) {
|
||
$contentPiece = "";
|
||
foreach ($json['delta']['content'] as $segment) {
|
||
if (isset($segment['text']['value'])) {
|
||
$contentPiece .= $segment['text']['value'];
|
||
}
|
||
}
|
||
$parsedMessage .= $contentPiece;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (empty(trim($parsedMessage))) {
|
||
logMessage("Парсинг не дал результата, используем сырой ответ.");
|
||
$parsedMessage = $finalMessage;
|
||
}
|
||
|
||
logMessage("DEBUG: Итоговый ответ от ассистента (stream):\n" . $parsedMessage);
|
||
|
||
// Попытка извлечь вердикт из ответа ассистента
|
||
$verdict = 'Вердикт не определён';
|
||
if (preg_match('/Вердикт:\s*(.+)$/mi', $parsedMessage, $matches)) {
|
||
$extractedVerdict = trim($matches[1]);
|
||
if (!empty($extractedVerdict)) {
|
||
$verdict = $extractedVerdict;
|
||
}
|
||
}
|
||
|
||
return [
|
||
"status" => "complete",
|
||
"content" => $parsedMessage,
|
||
"moderationVerdict" => $verdict
|
||
];
|
||
}
|
||
|
||
function generateReport($allResults) {
|
||
$report = "### Итоговый отчет\n\n";
|
||
foreach ($allResults as $result) {
|
||
$report .= "**Результат анализа:**\n" . ($result['analysis']['content'] ?? 'Нет данных') . "\n";
|
||
$report .= "**Вердикт:** " . ($result['analysis']['moderationVerdict'] ?? 'Не определен') . "\n\n";
|
||
}
|
||
return $report;
|
||
}
|
||
?>
|