input: from typing import List, Callable
  
  def sentiment_analysis_pipeline(data: pd.DataFrame, preprocessing_steps: List[Callable[[pd.DataFrame], pd.DataFrame]]) -> str:
      """ Perform sentiment analysis on product reviews by following a series of preprocessing steps provided as a list of functions.
      Each function in the list should take a DataFrame and return a DataFrame. The output is a classification report for the sentiment predictions.
      
      >>> data = pd.DataFrame({'review': ['Great product, highly recommended!', 'Terrible, do not buy.'], 'sentiment': ['positive', 'negative']})
      >>> def tokenize(df):
      ...     df['tokenised'] = df['review'].apply(word_tokenize)
      ...     return df
      >>> def remove_stopwords(df):
      ...     stop_words = set(stopwords.words('english'))
      ...     df['filtered'] = df['tokenised'].apply(lambda x: [val for val in x if val not in stop_words])
      ...     return df
      >>> def lemmatize(df):
      ...     wordnet_lemmatizer = WordNetLemmatizer()
      ...     df['normalised'] = df['filtered'].apply(lambda x: [wordnet_lemmatizer.lemmatize(val) for val in x])
      ...     return df
      >>> def create_features(df):
      ...     cv = CountVectorizer()
      ...     df['features'] = cv.fit_transform(df['normalised'].astype('str')).toarray()
      ...     return df
      >>> results = sentiment_analysis_pipeline(data, [tokenize, remove_stopwords, lemmatize, create_features])
      """
output:     for step in preprocessing_steps:
          data = step(data)
      
      X = data['features']
      y = data['sentiment']
      
      X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
      
      model = LogisticRegression()
      model.fit(X_train, y_train)
      
      y_pred = model.predict(X_test)
      
      report = classification_report(y_test, y_pred)
      
      return report