'Two Wheeler', '2 wheeler' => 'Two Wheeler', '2w' => 'Two Wheeler', 'motorcycle' => 'Two Wheeler', 'bike' => 'Two Wheeler', 'scooter' => 'Two Wheeler', 'moped' => 'Two Wheeler', 'four wheeler' => 'Four Wheeler', '4 wheeler' => 'Four Wheeler', '4w' => 'Four Wheeler', 'private car' => 'Four Wheeler', 'car' => 'Four Wheeler', 'jeep' => 'Four Wheeler', 'goods vehicle' => 'Goods Vehicle', 'good vehicle' => 'Goods Vehicle', 'goods carrier' => 'Goods Vehicle', 'goods carrying vehicle' => 'Goods Vehicle', 'cargo vehicle' => 'Goods Vehicle', 'lorry' => 'Goods Vehicle', 'truck' => 'Goods Vehicle', 'transport vehicle goods'=> 'Goods Vehicle', 'commercial vehicle' => 'Commercial Vehicle', 'commercial car' => 'Commercial Vehicle', 'taxi' => 'Commercial Vehicle', 'cab' => 'Commercial Vehicle', 'hire vehicle' => 'Commercial Vehicle', 'passenger commercial' => 'Commercial Vehicle', 'bus' => 'Bus', 'passenger bus' => 'Bus', 'school bus' => 'Bus', 'private bus' => 'Bus', 'stage carriage' => 'Bus', 'tractor' => 'Tractor', 'agricultural tractor' => 'Tractor', 'farm tractor' => 'Tractor', ]; protected array $fuelType = [ 'Petrol' => 'Petrol', 'Diesel' => 'Diesel', 'CNG' => 'CNG', 'LPG' => 'LPG', ]; public function __construct() { $this->PolicyModel = new PolicyModel(); } /** * Job handler: read policy PDF via RAG API, update DB, then calculate commission. */ public function readFileAndCalculateCommission(array $data): array { $policyId = $data['policy_id']; $this->logRagStep('JOB_START', 'started', 'readFileAndCalculateCommission', ['policy_id' => $policyId]); $readDoc = $this->readPolicyDocViaRag($policyId); $calculateCommission = ['status' => 'failed']; if ($readDoc['status'] === 'success') { $extracted = $readDoc['data']; $policyData = $this->mapExtractedDataToPolicyFields($extracted); $this->logRagStep('DB_UPDATE', 'started', 'Updating policy from RAG data', [ 'policy_id' => $policyId, 'payload' => $policyData, ]); $updateStatus = $this->PolicyModel->update($policyId, $policyData); if ($updateStatus) { $this->logRagStep('DB_UPDATE', 'success', 'Policy updated from RAG data', ['policy_id' => $policyId]); $this->logRagStep('COMMISSION', 'started', 'Calculating commission', ['policy_id' => $policyId]); $policyController = new PolicyController(); $calculateCommission = $policyController->calculateCommission($policyId); $commissionStatus = $calculateCommission['status'] ?? 'failed'; $this->logRagStep( 'COMMISSION', $commissionStatus === 'success' ? 'success' : 'failed', 'Commission calculation finished', ['policy_id' => $policyId, 'result' => $commissionStatus] ); } else { $this->logRagStep('DB_UPDATE', 'failed', 'Failed to update policy from RAG data', ['policy_id' => $policyId]); } } else { $this->logRagStep('JOB_READ', 'failed', 'readPolicyDocViaRag failed', [ 'policy_id' => $policyId, 'response' => $readDoc, ]); } $this->logRagStep('JOB_END', 'completed', 'readFileAndCalculateCommission finished', [ 'policy_id' => $policyId, 'readPolicyDocViaRag' => $readDoc['status'], 'calculateCommission' => $calculateCommission['status'] ?? 'failed', ]); return [ 'readPolicyDocViaRag' => $readDoc['status'], 'calculateCommission' => $calculateCommission['status'] ?? 'failed', ]; } /** * Test endpoint: ?type=read&policy_id=123 */ public function policyRagRead() { $type = $this->request->getGet('type'); $policyId = $this->request->getGet('policy_id'); if ($type === 'read') { $readDoc = $this->readPolicyDocViaRag($policyId); if ($readDoc['status'] === 'success') { $policyData = $this->mapExtractedDataToPolicyFields($readDoc['data']); $this->PolicyModel->update($policyId, $policyData); } return $this->respond(['status' => $readDoc['status'], 'data' => $readDoc], 200); } if ($type === 'commission') { $policyController = new PolicyController(); return $policyController->calculateCommission($policyId, true); } return $this->respond(['status' => 'failed', 'message' => 'Invalid type. Use type=read or type=commission'], 200); } /** * Upload PDF to RAG, poll until indexed, extract JSON via chat, then delete RAG file. */ public function readPolicyDocViaRag($policyId = null) { $fileId = null; $pdfFilePath = null; $this->logRagStep('READ_START', 'started', 'readPolicyDocViaRag', ['policy_id' => $policyId]); try { $record = $this->PolicyModel->find((int) $policyId); if (!$record) { $this->logRagStep('POLICY_LOOKUP', 'failed', 'Policy not found', ['policy_id' => $policyId]); return $this->formatReadResponse('failed', 'Policy not found'); } $this->logRagStep('POLICY_LOOKUP', 'success', 'Policy found', [ 'policy_id' => $policyId, 'pdf_file' => $record['policy_pdf_file_name'] ?? null, ]); $pdfFileName = $record['policy_pdf_file_name'] ?? ''; if ($pdfFileName === '' || !policy_exists_by_type($pdfFileName, 'policy_pdf')) { $missingPath = $pdfFileName !== '' ? storage()->resolveKey('policy', 'policy_pdf', $pdfFileName) : ''; $this->logRagStep('PDF_CHECK', 'failed', 'PDF file not found', ['path' => $missingPath]); return $this->formatReadResponse('failed', "File not found at {$missingPath}"); } $pdfFilePath = policy_local_pdf_path($pdfFileName); $this->logRagStep('PDF_CHECK', 'success', 'PDF file found', ['path' => $pdfFilePath]); $this->logRagStep('UPLOAD', 'started', 'Uploading PDF to RAG API', ['path' => $pdfFilePath]); $uploadResult = $this->uploadFileToRag($pdfFilePath, (int) $policyId); if ($uploadResult['status'] !== 'success') { return $this->formatReadResponse('failed', $uploadResult['message']); } $fileId = $uploadResult['data']['file_id'] ?? null; if (empty($fileId)) { $this->logRagStep('UPLOAD', 'failed', 'RAG upload did not return file_id', ['policy_id' => $policyId]); return $this->formatReadResponse('failed', 'RAG upload did not return file_id'); } $this->logRagStep('SAVE_RAG_FILE_ID', 'started', 'Saving rag_file_id', [ 'policy_id' => $policyId, 'file_id' => $fileId, ]); $updated = $this->PolicyModel->update((int) $policyId, ['rag_file_id' => $fileId]); if ($updated === false) { $this->logRagStep('SAVE_RAG_FILE_ID', 'failed', 'Failed to save rag_file_id', [ 'policy_id' => $policyId, 'file_id' => $fileId, ]); } else { $this->logRagStep('SAVE_RAG_FILE_ID', 'success', 'rag_file_id saved', [ 'policy_id' => $policyId, 'file_id' => $fileId, ]); } $this->logRagStep('INDEX_POLL', 'started', 'Waiting for RAG file to be indexed', [ 'policy_id' => $policyId, 'file_id' => $fileId, ]); $indexResult = $this->waitForFileIndexed($fileId, (int) $policyId); if ($indexResult['status'] !== 'success') { return $this->formatReadResponse('failed', $indexResult['message']); } $this->logRagStep('CHAT', 'started', 'Sending extraction prompt to RAG API', [ 'policy_id' => $policyId, 'file_id' => $fileId, ]); $chatResult = $this->chatWithRag($fileId, $this->getPolicyReadPrompt(), (int) $policyId); if ($chatResult['status'] !== 'success') { return $this->formatReadResponse('failed', $chatResult['message']); } $answer = $chatResult['data']['answer'] ?? ''; $finalData = $this->extractJsonFromText($answer, (int) $policyId); if ($finalData === null) { $this->logRagStep('JSON_EXTRACT', 'failed', 'Could not extract valid JSON from RAG answer', [ 'policy_id' => $policyId, 'file_id' => $fileId, 'answer_snippet' => substr($answer, 0, 500), ]); return $this->formatReadResponse('failed', 'Could not extract valid JSON from RAG answer'); } $this->logRagStep('READ_COMPLETE', 'success', 'Policy document read successfully', [ 'policy_id' => $policyId, 'file_id' => $fileId, ]); return $this->formatReadResponse('success', 'Doc read success', $finalData); } catch (\Throwable $e) { $this->logRagStep('READ_ERROR', 'failed', $e->getMessage(), [ 'policy_id' => $policyId, 'file_id' => $fileId, 'line' => $e->getLine(), ]); return $this->formatReadResponse('failed', 'Error: ' . $e->getMessage()); } finally { storage_cleanup_temp($pdfFilePath); if (!empty($fileId)) { $this->logRagStep('DELETE', 'started', 'Deleting RAG file', [ 'policy_id' => $policyId, 'file_id' => $fileId, ]); $deleteResult = $this->deleteRagFile($fileId, (int) $policyId); if ($deleteResult['status'] !== 'success') { $this->logRagStep('DELETE', 'failed', $deleteResult['message'] ?? 'RAG file delete failed', [ 'policy_id' => $policyId, 'file_id' => $fileId, ]); } } $this->logRagStep('READ_END', 'completed', 'readPolicyDocViaRag finished', [ 'policy_id' => $policyId, 'file_id' => $fileId, ]); } } private function getPolicyReadPrompt(): string { $sampleJson = json_encode([ 'policy_number' => '', 'issued_date' => '', 'start_date' => '', 'end_date' => '', 'broker_name' => '', 'insured_name' => '', 'reg_no' => '', 'rto_state_code' => '', 'rto_city_code' => '', 'weight' => '', 'fuel_type' => '', 'date_of_registration' => '', 'year_of_manufacture' => '', 'engine_no' => '', 'chassis_no' => '', 'make' => '', 'model' => '', 'cubic_capacity' => '', 'vehicle_type' => '', 'tp' => 0, 'od' => 0, 'pa' => 0, 'cgst' => 0, 'sgst' => 0, 'igst' => 0, 'premium_amount' => 0, ], JSON_UNESCAPED_SLASHES); return 'Read the motor policy document and return only valid JSON using this exact structure (no explanation): ' . $sampleJson . '. Rules: vehicle_type must be one of Two Wheeler, Four Wheeler, Goods Vehicle, Bus, Tractor, Commercial Vehicle. ' . 'rto_state_code is first 2 characters of reg_no. rto_city_code is 3rd and 4th characters of reg_no. ' . 'All dates must be in YYYY-MM-DD format. TP premium must not include PA amount.'; } private function mapExtractedDataToPolicyFields(array $data): array { $data = $this->normalizeExtractedData($data); $vehicleType = $data['vehicle_type'] ?? null; $fuelType = $data['fuel_type'] ?? null; if ($vehicleType !== null && $vehicleType !== '') { $vehicleType = $this->postProcessExtractedData((string) $vehicleType, $this->tier1VehicleTypeMap); } else { $vehicleType = 'UN_IDEN_DOC'; } if ($fuelType !== null && $fuelType !== '') { $fuelType = $this->postProcessExtractedData((string) $fuelType, $this->fuelType); } else { $fuelType = 'UN_IDEN_DOC'; } return [ 'policy_number' => $this->nullIfEmpty($data['policy_number'] ?? null), 'issued_date' => $this->nullIfEmpty($data['issued_date'] ?? null), 'start_date' => $this->nullIfEmpty($data['start_date'] ?? null), 'end_date' => $this->nullIfEmpty($data['end_date'] ?? null), 'broker_name' => $this->nullIfEmpty($data['broker_name'] ?? null), 'tp' => $this->nullIfEmpty($data['tp'] ?? null), 'od' => $this->nullIfEmpty($data['od'] ?? null), 'pa' => $this->nullIfEmpty($data['pa'] ?? null), 'cgst' => $this->nullIfEmpty($data['cgst'] ?? 0), 'sgst' => $this->nullIfEmpty($data['sgst'] ?? 0), 'igst' => $this->nullIfEmpty($data['igst'] ?? 0), 'premium_amount' => $this->nullIfEmpty($data['premium_amount'] ?? null), 'rto_state_code' => $this->nullIfEmpty($data['rto_state_code'] ?? null), 'rto_city_code' => $this->nullIfEmpty($data['rto_city_code'] ?? null), 'weight' => $this->nullIfEmpty($data['weight'] ?? null), 'fuel_type' => $fuelType, 'date_of_registration' => $this->nullIfEmpty($data['date_of_registration'] ?? null), 'year_of_manufacture' => $this->nullIfEmpty($data['year_of_manufacture'] ?? null), 'engine_no' => $this->nullIfEmpty($data['engine_no'] ?? null), 'chassis_no' => $this->nullIfEmpty($data['chassis_no'] ?? null), 'make' => $this->nullIfEmpty($data['make'] ?? null), 'model' => $this->nullIfEmpty($data['model'] ?? null), 'cubic_capacity' => $this->nullIfEmpty($data['cubic_capacity'] ?? null), 'product' => $vehicleType, 'rc_no' => $this->nullIfEmpty($data['reg_no'] ?? null), 'insured_name' => $this->nullIfEmpty($data['insured_name'] ?? null), ]; } private function normalizeExtractedData(array $data): array { if (!isset($data['policy']) && !isset($data['vehicle']) && !isset($data['premium'])) { return $data; } return [ 'policy_number' => $data['policy']['policy_number'] ?? null, 'issued_date' => $data['policy']['issue_date'] ?? null, 'start_date' => $data['policy']['period']['start'] ?? null, 'end_date' => $data['policy']['period']['end'] ?? null, 'broker_name' => $data['policy']['intermediary_name'] ?? null, 'insured_name' => $data['insured']['name'] ?? null, 'reg_no' => $data['vehicle']['reg_no'] ?? null, 'rto_state_code' => $data['vehicle']['rto_state_code'] ?? null, 'rto_city_code' => $data['vehicle']['rto_city_code'] ?? null, 'weight' => $data['vehicle']['weight'] ?? null, 'fuel_type' => $data['vehicle']['fuel_type'] ?? null, 'date_of_registration' => $data['vehicle']['date_of_registration'] ?? null, 'year_of_manufacture' => $data['vehicle']['year_of_manufacture'] ?? null, 'engine_no' => $data['vehicle']['engine_no'] ?? null, 'chassis_no' => $data['vehicle']['chassis_no'] ?? null, 'make' => $data['vehicle']['make'] ?? null, 'model' => $data['vehicle']['model'] ?? null, 'cubic_capacity' => $data['vehicle']['cubic_capacity'] ?? null, 'vehicle_type' => $data['vehicle']['vehicle_type'] ?? null, 'tp' => $data['premium']['tp'] ?? null, 'od' => $data['premium']['od'] ?? null, 'pa' => $data['premium']['pa'] ?? null, 'cgst' => $data['premium']['taxes']['cgst'] ?? 0, 'sgst' => $data['premium']['taxes']['sgst'] ?? 0, 'igst' => $data['premium']['taxes']['igst'] ?? 0, 'premium_amount' => $data['premium']['total'] ?? null, ]; } private function nullIfEmpty($value) { if ($value === '' || $value === null) { return null; } return $value; } private function postProcessExtractedData(string $incoming, array $masterArray): string { $value = strtolower(trim($incoming)); $value = preg_replace('/\s+/', ' ', $value); return $masterArray[$value] ?? 'UN_IDEN_TYPE'; } private function ragApiBaseUrl(): string { $baseUrl = rtrim((string) getenv('RAG_API_BASE_URL'), '/'); if ($baseUrl === '') { throw new \RuntimeException('RAG_API_BASE_URL is not configured'); } return $baseUrl; } private function uploadFileToRag(string $pdfFilePath, ?int $policyId = null): array { $url = $this->ragApiBaseUrl() . '/api/files/upload'; $cfile = new \CURLFile($pdfFilePath, 'application/pdf', basename($pdfFilePath)); $ch = curl_init($url); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, [ 'uploaded_file' => $cfile, 'auto_detect' => 'true', ]); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = curl_exec($ch); $curlError = curl_error($ch); $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($curlError) { $this->logRagStep('UPLOAD', 'failed', 'cURL error: ' . $curlError, [ 'policy_id' => $policyId, 'url' => $url, ]); return ['status' => 'failed', 'message' => 'RAG upload cURL error: ' . $curlError]; } if ($httpCode < 200 || $httpCode >= 300) { $this->logRagStep('UPLOAD', 'failed', "HTTP {$httpCode}", [ 'policy_id' => $policyId, 'response' => substr((string) $response, 0, 500), ]); return ['status' => 'failed', 'message' => "RAG upload failed with HTTP {$httpCode}"]; } $responseData = json_decode((string) $response, true); if (!is_array($responseData) || empty($responseData['success'])) { $this->logRagStep('UPLOAD', 'failed', 'Invalid API response', [ 'policy_id' => $policyId, 'response' => substr((string) $response, 0, 500), ]); return ['status' => 'failed', 'message' => 'RAG upload returned invalid response']; } $this->logRagStep('UPLOAD', 'success', 'PDF uploaded to RAG API', [ 'policy_id' => $policyId, 'file_id' => $responseData['file_id'] ?? null, 'filename' => $responseData['filename'] ?? null, 'status' => $responseData['status'] ?? null, ]); return ['status' => 'success', 'data' => $responseData]; } private function getFileStatus(string $fileId, ?int $policyId = null): array { $url = $this->ragApiBaseUrl() . '/api/files/status?file_id=' . urlencode($fileId); $ch = curl_init($url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = curl_exec($ch); $curlError = curl_error($ch); $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($curlError) { $this->logRagStep('INDEX_POLL', 'failed', 'Status check cURL error: ' . $curlError, [ 'policy_id' => $policyId, 'file_id' => $fileId, ]); return ['status' => 'failed', 'message' => 'RAG status cURL error: ' . $curlError]; } if ($httpCode < 200 || $httpCode >= 300) { $this->logRagStep('INDEX_POLL', 'failed', "Status check HTTP {$httpCode}", [ 'policy_id' => $policyId, 'file_id' => $fileId, ]); return ['status' => 'failed', 'message' => "RAG status check failed with HTTP {$httpCode}"]; } $responseData = json_decode((string) $response, true); if (!is_array($responseData) || empty($responseData['success'])) { $this->logRagStep('INDEX_POLL', 'failed', 'Status check invalid response', [ 'policy_id' => $policyId, 'file_id' => $fileId, ]); return ['status' => 'failed', 'message' => 'RAG status returned invalid response']; } return ['status' => 'success', 'data' => $responseData]; } private function waitForFileIndexed(string $fileId, ?int $policyId = null): array { $interval = (int) (getenv('RAG_API_STATUS_POLL_INTERVAL') ?: 10); $maxAttempts = (int) (getenv('RAG_API_STATUS_MAX_ATTEMPTS') ?: 60); for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) { $statusResult = $this->getFileStatus($fileId, $policyId); if ($statusResult['status'] !== 'success') { return $statusResult; } $fileStatus = strtolower((string) ($statusResult['data']['status'] ?? '')); $this->logRagStep('INDEX_POLL', 'progress', "Poll attempt {$attempt}/{$maxAttempts}", [ 'policy_id' => $policyId, 'file_id' => $fileId, 'status' => $fileStatus, ]); if ($fileStatus === 'indexed') { $this->logRagStep('INDEX_POLL', 'success', 'File indexed', [ 'policy_id' => $policyId, 'file_id' => $fileId, 'chunk_count' => $statusResult['data']['chunk_count'] ?? null, 'attempts' => $attempt, ]); return $statusResult; } if (in_array($fileStatus, ['failed', 'error'], true)) { $this->logRagStep('INDEX_POLL', 'failed', 'Indexing failed', [ 'policy_id' => $policyId, 'file_id' => $fileId, 'status' => $fileStatus, ]); return ['status' => 'failed', 'message' => 'RAG file indexing failed with status: ' . $fileStatus]; } if ($attempt < $maxAttempts) { sleep($interval); } } $this->logRagStep('INDEX_POLL', 'failed', 'Indexing timed out', [ 'policy_id' => $policyId, 'file_id' => $fileId, 'max_attempts' => $maxAttempts, 'interval_sec' => $interval, ]); return ['status' => 'failed', 'message' => 'RAG file indexing timed out']; } private function chatWithRag(string $fileId, string $question, ?int $policyId = null): array { $url = $this->ragApiBaseUrl() . '/api/chat'; $payload = json_encode([ 'question' => $question, 'file_id' => $fileId, ]); $ch = curl_init($url); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = curl_exec($ch); $curlError = curl_error($ch); $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($curlError) { $this->logRagStep('CHAT', 'failed', 'cURL error: ' . $curlError, [ 'policy_id' => $policyId, 'file_id' => $fileId, ]); return ['status' => 'failed', 'message' => 'RAG chat cURL error: ' . $curlError]; } if ($httpCode < 200 || $httpCode >= 300) { $this->logRagStep('CHAT', 'failed', "HTTP {$httpCode}", [ 'policy_id' => $policyId, 'file_id' => $fileId, 'response' => substr((string) $response, 0, 500), ]); return ['status' => 'failed', 'message' => "RAG chat failed with HTTP {$httpCode}"]; } $responseData = json_decode((string) $response, true); if (!is_array($responseData) || empty($responseData['success'])) { $this->logRagStep('CHAT', 'failed', 'Invalid API response', [ 'policy_id' => $policyId, 'file_id' => $fileId, ]); return ['status' => 'failed', 'message' => 'RAG chat returned invalid response']; } $this->logRagStep('CHAT', 'success', 'RAG chat response received', [ 'policy_id' => $policyId, 'file_id' => $fileId, 'answer_length' => strlen((string) ($responseData['answer'] ?? '')), ]); return ['status' => 'success', 'data' => $responseData]; } private function deleteRagFile(string $fileId, ?int $policyId = null): array { $url = $this->ragApiBaseUrl() . '/api/files/id/' . urlencode($fileId); $ch = curl_init($url); curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE'); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = curl_exec($ch); $curlError = curl_error($ch); $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($curlError) { $this->logRagStep('DELETE', 'failed', 'cURL error: ' . $curlError, [ 'policy_id' => $policyId, 'file_id' => $fileId, ]); return ['status' => 'failed', 'message' => 'RAG delete cURL error: ' . $curlError]; } if ($httpCode < 200 || $httpCode >= 300) { $this->logRagStep('DELETE', 'failed', "HTTP {$httpCode}", [ 'policy_id' => $policyId, 'file_id' => $fileId, 'response' => substr((string) $response, 0, 500), ]); return ['status' => 'failed', 'message' => "RAG delete failed with HTTP {$httpCode}"]; } $responseData = json_decode((string) $response, true); if (!is_array($responseData) || empty($responseData['success'])) { $this->logRagStep('DELETE', 'failed', 'Invalid API response', [ 'policy_id' => $policyId, 'file_id' => $fileId, ]); return ['status' => 'failed', 'message' => 'RAG delete returned invalid response']; } $this->logRagStep('DELETE', 'success', 'RAG file deleted', [ 'policy_id' => $policyId, 'file_id' => $fileId, ]); return ['status' => 'success', 'data' => $responseData]; } private function extractJsonFromText(string $text, ?int $policyId = null): ?array { $jsonString = ''; if (preg_match('/```json\s*(.*?)\s*```/s', $text, $matches)) { $jsonString = trim($matches[1]); } else { $start = strpos($text, '{'); $end = strrpos($text, '}'); if ($start !== false && $end !== false && $end > $start) { $jsonString = substr($text, $start, $end - $start + 1); } } if ($jsonString === '') { return null; } $decoded = json_decode($jsonString, true); if (!is_array($decoded)) { $this->logRagStep('JSON_EXTRACT', 'failed', 'JSON decode failed', [ 'policy_id' => $policyId, 'json_snippet' => substr($jsonString, 0, 500), ]); return null; } $this->logRagStep('JSON_EXTRACT', 'success', 'JSON extracted from RAG answer', [ 'policy_id' => $policyId, 'field_keys' => array_keys($decoded), ]); return $decoded; } private function logRagStep(string $step, string $status, string $message, array $context = []): void { $level = in_array($status, ['failed', 'error'], true) ? 'error' : 'info'; $contextJson = $context ? ' | ' . json_encode($context, JSON_UNESCAPED_SLASHES) : ''; log_message($level, "[PolicyRag][{$step}][{$status}] {$message}{$contextJson}"); } private function formatReadResponse(string $status, string $message, ?array $data = null): array { $result = ['status' => $status, 'message' => $message]; if ($data !== null) { $result['data'] = $data; } return $result; } }