singleBatchLimit); } $input = preg_replace('/[;|\\t\\n\\r\\\\\\/:\\-_ ]+/', ',', $input); $smiles = explode(',', $input); $smiles = array_map('trim', $smiles); $smiles = array_filter($smiles, fn($value) => $value !== '' && $value !== null); $smiles = array_values(array_unique($smiles)); return array_slice($smiles, 0, $this->singleBatchLimit); } protected function parseFileContent(string $content, string $extension): array { $lines = explode("\n", $content); $smilesList = []; foreach ($lines as $line) { $line = trim($line); if (empty($line) || str_starts_with($line, '#')) continue; if ($extension === 'csv') { if (str_contains(strtolower($line), 'smiles')) continue; $row = str_getcsv($line); $smiles = trim($row[0] ?? ''); } else { $smiles = $line; } if (!empty($smiles)) { $smilesList[] = $smiles; } } return array_slice(array_values(array_unique($smilesList)), 0, $this->batchSize); } public function predictFromFile(string $fileContent, string $extension): array { $smilesList = $this->parseFileContent($fileContent, $extension); if (empty($smilesList)) { throw new \Exception('No valid SMILES found in the uploaded file'); } return $this->processSmilesList($smilesList, $extension); } public function predictBatchFromString(string $input) { $smilesList = $this->parseSmiles($input); if (empty($smilesList)) { throw new \Exception('No valid SMILES provided'); } return $this->processSmilesList($smilesList); } public function predictSingle(string $smiles) { // البحث بدون user_id $existing = Admet::where('smiles', $smiles)->first(); if ($existing) { return $this->formatResult($existing, $smiles); } return $this->predictBatchFromString($smiles); } protected function processSmilesList(array $smilesList, ?string $fileType = null): array { [$dbResults, $missingSmiles] = $this->getExistingPredictions($smilesList); $apiResults = []; if (!empty($missingSmiles)) { $apiResults = $this->fetchFromApi($missingSmiles); } $result = [ 'total_processed' => count($dbResults) + count($apiResults), 'total_smiles' => count($smilesList), 'results' => array_merge($dbResults, $apiResults) ]; if ($fileType) { $result['file_type'] = $fileType; } return $result; } protected function getExistingPredictions(array $smilesList): array { $dbResults = []; $missingSmiles = []; foreach ($smilesList as $smiles) { // إزالة التصفية حسب user_id $existing = Admet::where('smiles', $smiles)->first(); if ($existing) { $dbResults[] = array_merge($this->formatResult($existing, $smiles), ['source' => 'database']); } else { $missingSmiles[] = $smiles; } } return [$dbResults, $missingSmiles]; } protected function fetchFromApi(array $smilesList): array { if (empty($smilesList)) { return []; } try { $response = Http::timeout($this->timeout)->post( config('services.admet.url') . '/predict/batch', ['smiles_list' => $smilesList] ); if (!$response->successful()) { \Log::error('ADMET API request failed', [ 'status' => $response->status(), 'body' => $response->body(), ]); return $this->createErrorResults($smilesList, 'API request failed: ' . $response->status()); } $data = $response->json(); $results_data = isset($data['results']) ? $data : ['results' => $data]; return $this->saveApiResults($results_data, $smilesList); } catch (\Exception $e) { \Log::error('ADMET API exception', ['error' => $e->getMessage()]); return $this->createErrorResults($smilesList, $e->getMessage()); } } protected function saveApiResults(array $data, array $smilesList): array { $results = []; if (isset($data['results']) && is_array($data['results'])) { foreach ($data['results'] as $index => $result) { if (!isset($smilesList[$index])) continue; $smiles = $smilesList[$index]; $predictions_data = $result['predictions'] ?? $result; if (!empty($result['error']) || empty($predictions_data)) { $results[] = [ 'smiles' => $smiles, 'error' => $result['error'] ?? 'No predictions returned', 'source' => 'api', ]; continue; } // حفظ بدون user_id $admet = $this->saveToDatabase($smiles, $predictions_data); $results[] = array_merge($this->formatResult($admet, $smiles), ['source' => 'api']); } } else { \Log::warning('Unexpected ADMET API response format', ['data' => $data]); return $this->createErrorResults($smilesList, 'Invalid response format from API'); } return $results; } protected function saveToDatabase(string $smiles, array $predictions): Admet { // إزالة user_id من updateOrCreate return Admet::updateOrCreate( ['smiles' => $smiles], // فقط smiles للبحث [ 'absorption' => $predictions['Absorption'] ?? $predictions['absorption'] ?? null, 'distribution' => $predictions['Distribution'] ?? $predictions['distribution'] ?? null, 'metabolism' => $predictions['Metabolism'] ?? $predictions['metabolism'] ?? null, 'excretion' => $predictions['Excretion'] ?? $predictions['excretion'] ?? null, 'toxicity' => $predictions['Toxicity'] ?? $predictions['toxicity'] ?? null, ] ); } protected function createErrorResults(array $smilesList, string $error): array { $errors = []; foreach ($smilesList as $smiles) { $errors[] = [ 'smiles' => $smiles, 'error' => $error, 'source' => 'error' ]; } return $errors; } protected function formatResult(Admet $admet, string $smiles): array { return [ 'smiles' => $smiles, 'absorption' => (float) ($admet->absorption ?? 0), 'distribution' => (float) ($admet->distribution ?? 0), 'metabolism' => (float) ($admet->metabolism ?? 0), 'excretion' => (float) ($admet->excretion ?? 0), 'toxicity' => (float) ($admet->toxicity ?? 0), ]; } // ==================== نظام قائمة الانتظار ==================== protected function waitForResult(string $requestId, int $timeout = 30): array { $start = microtime(true); while (true) { $result = Cache::get("admet_result_$requestId"); if ($result !== null) { Cache::forget("admet_result_$requestId"); return $result; } if ((microtime(true) - $start) > $timeout) { throw new \Exception('Timeout waiting for result after ' . $timeout . ' seconds'); } usleep(100000); } } protected function tryProcessBatch(): void { $lock = Cache::lock($this->lockKey, 60); if ($lock->get()) { try { usleep(10000); $this->processBatch(); } finally { $lock->release(); } } } protected function processBatch(): void { $requests = []; while (($item = Redis::lpop($this->queueKey)) !== null) { $requests[] = json_decode($item, true); } if (empty($requests)) { return; } [$allSmiles, $smilesToRequestMap] = $this->buildRequestMap($requests); if (empty($allSmiles)) { return; } try { $response = Http::timeout($this->timeout)->post( config('services.admet.url') . '/predict/batch', ['smiles_list' => $allSmiles] ); if (!$response->successful()) { throw new \Exception('Batch request failed: ' . $response->body()); } $data = $response->json(); $apiResults = $data['results'] ?? []; $resultsPerRequest = $this->distributeBatchResults($apiResults, $smilesToRequestMap); foreach ($resultsPerRequest as $requestId => $requestResults) { $payload = [ 'total_processed' => count($requestResults), 'total_smiles' => count($requestResults), 'results' => $requestResults, ]; Cache::put("admet_result_$requestId", $payload, 60); } $this->handleMissingRequests($requests, $resultsPerRequest); } catch (\Exception $e) { \Log::error('ADMET batch processing failed: ' . $e->getMessage()); foreach ($requests as $req) { Cache::put("admet_result_{$req['id']}", [ 'error' => 'Batch processing failed: ' . $e->getMessage() ], 60); } throw $e; } } protected function buildRequestMap(array $requests): array { $smilesToRequestMap = []; $allSmiles = []; foreach ($requests as $reqIndex => $req) { foreach ($req['smiles_list'] as $smilesIndex => $smiles) { $smilesKey = count($allSmiles); $allSmiles[] = $smiles; $smilesToRequestMap[$smilesKey] = [ 'request_id' => $req['id'], 'request_index' => $reqIndex, 'smiles_index' => $smilesIndex, 'smiles' => $smiles, // إزالة user_id من هنا ]; } } return [$allSmiles, $smilesToRequestMap]; } protected function distributeBatchResults(array $apiResults, array $smilesToRequestMap): array { $resultsPerRequest = []; foreach ($apiResults as $resultIndex => $result) { if (!isset($smilesToRequestMap[$resultIndex])) { continue; } $map = $smilesToRequestMap[$resultIndex]; $requestId = $map['request_id']; $predictions = $result['predictions'] ?? $result; // حفظ بدون user_id $admet = $this->saveToDatabase($map['smiles'], $predictions); $resultsPerRequest[$requestId][] = $this->formatResult($admet, $map['smiles']); } return $resultsPerRequest; } protected function handleMissingRequests(array $requests, array $resultsPerRequest): void { foreach ($requests as $req) { if (!isset($resultsPerRequest[$req['id']])) { Cache::put("admet_result_{$req['id']}", [ 'error' => 'No predictions received for any SMILES', 'smiles_list' => $req['smiles_list'] ], 60); } } } public function getBatchStatus(string $requestId) { return Cache::get("admet_result_$requestId"); } public function clearOldResults(int $olderThanMinutes = 60): void { $keys = Redis::keys('admet_result_*'); if (empty($keys)) { return; } foreach ($keys as $key) { $ttl = Redis::ttl($key); if ($ttl < 0 || $ttl > ($olderThanMinutes * 60)) { Redis::del($key); } } } }