0) { @ob_end_clean(); } ob_implicit_flush(true); header('Content-Type: application/x-ndjson; charset=utf-8'); header('Cache-Control: no-store'); header('X-Accel-Buffering: no'); $language = 'en'; $startTime = microtime(true); $emit = function (string $event, array $payload = []) use ($startTime): void { $payload['event'] = $event; $payload['t_ms'] = (int)round((microtime(true) - $startTime) * 1000); echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n"; @flush(); }; try { // Parse payload (always multipart — two files required) $payloadRaw = (string)($_POST['payload'] ?? ''); if ($payloadRaw === '') { throw new DbnToolsHttpException('Missing payload field.', 422, 'missing_payload'); } $input = json_decode($payloadRaw, true); if (!is_array($input)) { throw new DbnToolsHttpException('Invalid payload JSON.', 422, 'invalid_payload_json'); } $language = dbnToolsNormalizeLanguage($input['language'] ?? 'en'); $engine = ToolModels::engineForUser($ftUid, (string)($input['engine'] ?? 'azure_mini')); $sliceInput = $input['slices'] ?? []; // Extract file A $emit('progress', ['detail' => 'Reading Document A…']); $fileEntryA = $_FILES['file_a'] ?? null; if (!$fileEntryA || ($fileEntryA['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) { throw new DbnToolsHttpException( 'Document A is required. Upload a PDF, DOCX, or TXT file.', 422, 'missing_file_a' ); } $extractedA = dbnToolsExtractUploadedFile([ 'name' => $fileEntryA['name'] ?? '', 'type' => $fileEntryA['type'] ?? '', 'tmp_name' => $fileEntryA['tmp_name'] ?? '', 'error' => $fileEntryA['error'] ?? UPLOAD_ERR_NO_FILE, 'size' => $fileEntryA['size'] ?? 0, ]); $fileA = [ 'filename' => $extractedA['filename'], 'text' => $extractedA['text'], 'chars' => $extractedA['chars'], 'truncated' => $extractedA['truncated'], ]; $emit('progress', ['detail' => sprintf('Document A extracted: %s (%d chars%s)', $extractedA['filename'], $extractedA['chars'], !empty($extractedA['truncated']) ? ', truncated' : '')]); // Extract file B $emit('progress', ['detail' => 'Reading Document B…']); $fileEntryB = $_FILES['file_b'] ?? null; if (!$fileEntryB || ($fileEntryB['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) { throw new DbnToolsHttpException( 'Document B is required. Upload a PDF, DOCX, or TXT file.', 422, 'missing_file_b' ); } $extractedB = dbnToolsExtractUploadedFile([ 'name' => $fileEntryB['name'] ?? '', 'type' => $fileEntryB['type'] ?? '', 'tmp_name' => $fileEntryB['tmp_name'] ?? '', 'error' => $fileEntryB['error'] ?? UPLOAD_ERR_NO_FILE, 'size' => $fileEntryB['size'] ?? 0, ]); $fileB = [ 'filename' => $extractedB['filename'], 'text' => $extractedB['text'], 'chars' => $extractedB['chars'], 'truncated' => $extractedB['truncated'], ]; $emit('progress', ['detail' => sprintf('Document B extracted: %s (%d chars%s)', $extractedB['filename'], $extractedB['chars'], !empty($extractedB['truncated']) ? ', truncated' : '')]); if (($fileA['text'] ?? '') === '') { throw new DbnToolsHttpException('Could not extract text from Document A.', 422, 'empty_file_a'); } if (($fileB['text'] ?? '') === '') { throw new DbnToolsHttpException('Could not extract text from Document B.', 422, 'empty_file_b'); } $emit('start', [ 'engine' => $engine, 'language' => $language, 'file_a' => $fileA['filename'], 'file_b' => $fileB['filename'], ]); // Optional: append the user's case-context as supplementary background to Doc A. $useMyCase = !empty($input['use_my_case']); if ($useMyCase) { $retrievalQuery = mb_substr((string)$fileA['text'], 0, 2000, 'UTF-8'); $caseBlock = dbnToolsCaseContext(true, $retrievalQuery, 5); if ($caseBlock !== '') { $fileA['text'] .= "\n\n" . $caseBlock; } } $result = (new DbnDiscrepancyAgent())->run( $fileA, $fileB, $engine, $language, is_array($sliceInput) ? $sliceInput : [], $emit ); $result['ok'] = true; $result['latency_ms'] = (int)round((microtime(true) - $startTime) * 1000); dbnToolsLogMetadata([ 'tool' => 'discrepancy', 'language' => $language, 'ok' => true, 'latency_ms' => $result['latency_ms'], 'source_count' => (int)($result['trace_metadata']['source_count'] ?? 0), 'conflict_count' => (int)($result['trace_metadata']['conflict_count'] ?? 0), 'deleted_count' => (int)($result['trace_metadata']['deleted_count'] ?? 0), 'added_count' => (int)($result['trace_metadata']['added_count'] ?? 0), 'deployment' => $result['trace_metadata']['deployment'] ?? null, ]); $ftRemaining = dbnToolsFreeTierDeduct($ftUid, 'discrepancy'); if ($ftRemaining >= 0) { $result['balance'] = $ftRemaining; } $emit('final', ['result' => $result]); } catch (DbnToolsHttpException $e) { $latency = (int)round((microtime(true) - $startTime) * 1000); dbnToolsLogMetadata([ 'tool' => 'discrepancy', 'language' => $language, 'ok' => false, 'latency_ms' => $latency, 'error_code' => $e->errorCode, ]); $emit('error', ['code' => $e->errorCode, 'message' => $e->getMessage(), 'status' => $e->status]); } catch (Throwable $e) { error_log('DBN discrepancy fatal: ' . $e->getMessage()); $latency = (int)round((microtime(true) - $startTime) * 1000); dbnToolsLogMetadata([ 'tool' => 'discrepancy', 'language' => $language, 'ok' => false, 'latency_ms' => $latency, 'error_code' => 'internal_error', ]); $emit('error', ['code' => 'internal_error', 'message' => 'The discrepancy finder could not complete this request.']); }