MatPDF-Companion / paper_reading.py
cs-mubashir's picture
Upload 5 files
4f7f1a1 verified
import requests
import PyPDF2
import re
import os
import requests
import pandas as pd
import tiktoken
import time
from io import StringIO
from groq import Groq
import numpy as np
api_key='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'
client = Groq(api_key=api_key)
def count_tokens(text):
"""Returns the number of tokens in a text string."""
encoding = tiktoken.get_encoding("cl100k_base")
num_tokens = len(encoding.encode(text))
return num_tokens
def get_pdf_files(folder_path):
"""
Retrieve PDF files from the specified folder path with improved error handling.
Args:
folder_path (str): Path to the folder containing PDF files
Returns:
list: List of full paths to PDF files
"""
# Validate folder path
if not os.path.exists(folder_path):
raise ValueError(f"Folder path does not exist: {folder_path}")
# List to store PDF file paths
pdf_files = []
# Walk through directory
for root, dirs, files in os.walk(folder_path):
for file in files:
# Check if file is a PDF
if file.lower().endswith('.pdf'):
full_path = os.path.join(root, file)
pdf_files.append(full_path)
# Check if any PDFs were found
if not pdf_files:
raise ValueError(f"No PDF files found in the folder: {folder_path}")
return pdf_files
def get_txt_from_pdf(pdf_files, filter_ref=False):
data = []
for pdf in pdf_files:
try:
with open(pdf, 'rb') as pdf_content:
pdf_reader = PyPDF2.PdfReader(pdf_content)
for page_num in range(len(pdf_reader.pages)):
page = pdf_reader.pages[page_num]
page_text = page.extract_text()
words = page_text.split()
page_text_join = ' '.join(words)
if filter_ref:
page_text_join = remove_ref(page_text_join)
page_len = len(page_text_join)
div_len = page_len // 4 # Divide the page into 4 parts
page_parts = [page_text_join[i*div_len:(i+1)*div_len] for i in range(4)]
min_tokens = 40
for i, page_part in enumerate(page_parts):
if count_tokens(page_part) > min_tokens:
# Append the data to the list
data.append({
'file name': os.path.basename(pdf),
'page number': page_num + 1,
'page section': i+1,
'content': page_part,
'tokens': count_tokens(page_part)
})
except Exception as e:
print(f"Error processing {pdf}: {e}")
# Create a DataFrame from the data
df = pd.DataFrame(data)
return df
def remove_ref(pdf_text):
pattern = r'(REFERENCES|Acknowledgment|ACKNOWLEDGMENT)'
match = re.search(pattern, pdf_text)
if match:
# If a match is found, remove everything after the match
start_index = match.start()
clean_text = pdf_text[:start_index].strip()
else:
# Define a list of regular expression patterns for references
reference_patterns = [
'\[[\d\w]{1,3}\].+?[\d]{3,5}\.','\[[\d\w]{1,3}\].+?[\d]{3,5};','\([\d\w]{1,3}\).+?[\d]{3,5}\.','\[[\d\w]{1,3}\].+?[\d]{3,5},',
'\([\d\w]{1,3}\).+?[\d]{3,5},','\[[\d\w]{1,3}\].+?[\d]{3,5}','[\d\w]{1,3}\).+?[\d]{3,5}\.','[\d\w]{1,3}\).+?[\d]{3,5}',
'\([\d\w]{1,3}\).+?[\d]{3,5}','^[\w\d,\.– ;)-]+$',
]
# Find and remove matches with the first eight patterns
for pattern in reference_patterns[:8]:
matches = re.findall(pattern, pdf_text, flags=re.S)
pdf_text = re.sub(pattern, '', pdf_text) if len(matches) > 500 and matches.count('.') < 2 and matches.count(',') < 2 and not matches[-1].isdigit() else pdf_text
# Split the text into lines
lines = pdf_text.split('\n')
# Strip each line and remove matches with the last two patterns
for i, line in enumerate(lines):
lines[i] = line.strip()
for pattern in reference_patterns[7:]:
matches = re.findall(pattern, lines[i])
lines[i] = re.sub(pattern, '', lines[i]) if len(matches) > 500 and len(re.findall('\d', matches)) < 8 and len(set(matches)) > 10 and matches.count(',') < 2 and len(matches) > 20 else lines[i]
# Join the lines back together, excluding any empty lines
clean_text = '\n'.join([line for line in lines if line])
return clean_text
def split_content(input_string, tokens):
"""Splits a string into chunks based on a maximum token count. """
MAX_TOKENS = tokens
split_strings = []
current_string = ""
tokens_so_far = 0
for word in input_string.split():
# Check if adding the next word would exceed the max token limit
if tokens_so_far + count_tokens(word) > MAX_TOKENS:
# If we've reached the max tokens, look for the last dot or newline in the current string
last_dot = current_string.rfind(".")
last_newline = current_string.rfind("\n")
# Find the index to cut the current string
cut_index = max(last_dot, last_newline)
# If there's no dot or newline, we'll just cut at the max tokens
if cut_index == -1:
cut_index = MAX_TOKENS
# Add the substring to the result list and reset the current string and tokens_so_far
split_strings.append(current_string[:cut_index + 1].strip())
current_string = current_string[cut_index + 1:].strip()
tokens_so_far = count_tokens(current_string)
# Add the current word to the current string and update the token count
current_string += " " + word
tokens_so_far += count_tokens(word)
# Add the remaining current string to the result list
split_strings.append(current_string.strip())
return split_strings
def combine_section(df):
"""Merge sections, page numbers, add up content, and tokens based on the pdf name."""
aggregated_df = df.groupby('file name').agg({
'content': aggregate_content,
'tokens': aggregate_tokens
}).reset_index()
return aggregated_df
def combine_main_SI(df):
"""Create a new column with the main part of the file name, group the DataFrame by the new column,
and aggregate the content and tokens."""
df['main_part'] = df['file name'].apply(extract_title)
merged_df = df.groupby('main_part').agg({
'content': ''.join,
'tokens': sum
}).reset_index()
return merged_df.rename(columns={'main_part': 'file name'})
def aggregate_content(series):
"""Join all elements in the series with a space separator. """
return ' '.join(series)
def aggregate_tokens(series):
"""Sum all elements in the series."""
return series.sum()
def extract_title(file_name):
"""Extract the main part of the file name. """
title = file_name.split('_')[0]
return title.rstrip('.pdf')
def model_1(df):
"""Model 1 will turn text in dataframe to a summarized reaction condition table."""
# Initialize Groq client
response_msgs = []
for index, row in df.iterrows():
column1_value = row[df.columns[0]]
column2_value = row['content']
max_tokens = 3000
if count_tokens(column2_value) > max_tokens:
context_list = split_content(column2_value, max_tokens)
else:
context_list = [column2_value]
answers = '' # Collect answers from Groq
for context in context_list:
print("Start to analyze paper " + str(column1_value))
user_prompt = f"""This is an experimental section on MOF synthesis from paper {column1_value}
Context:
{context}
Q: Can you summarize the following details in a table:
compound name or chemical formula (if the name is not provided), metal source, metal amount, organic linker(s),
linker amount, modulator, modulator amount or volume, solvent(s), solvent volume(s), reaction temperature,
and reaction time?
Rules:
- If any information is not provided or you are unsure, use "N/A"
- Focus on extracting experimental conditions from only the MOF synthesis
- Ignore information related to organic linker synthesis, MOF postsynthetic modification, high throughput (HT) experiment details or catalysis reactions
- If multiple conditions are provided for the same compound, use multiple rows to represent them
- If multiple units or components are provided for the same factor (e.g., g and mol for the weight, multiple linker or metals, multiple temperature and reaction time, mixed solvents, etc), include them in the same cell and separate by comma
- The table should have 11 columns, all in lowercase:
| compound name | metal source | metal amount | linker | linker amount | modulator | modulator amount or volume | solvent | solvent volume | reaction temperature | reaction time |
Respond with ONLY the table."""
attempts = 3
while attempts > 0:
try:
response = client.chat.completions.create(
model="llama-3.1-70b-versatile", # or another available Groq model
messages=[
{"role": "system", "content": "You are a helpful assistant specialized in extracting MOF synthesis details."},
{"role": "user", "content": user_prompt}
]
)
answers_text = response.choices[0].message.content
# Check if response is valid
if answers_text and not answers_text.lower().startswith("i apologize"):
answers += '\n' + answers_text
break
else:
raise ValueError("Invalid or apologetic response")
except Exception as e:
attempts -= 1
if attempts <= 0:
print(f"Error: Failed to process paper {column1_value}. Skipping. (model 1)")
break
print(f"Error: {str(e)}. Retrying in 60 seconds. {attempts} attempts remaining. (model 1)")
time.sleep(60)
response_msgs.append(answers)
df = df.copy()
df.loc[:, 'summarized'] = response_msgs
return df
def model_2(df):
"""Model 2 identifies experiment sections and combines results"""
response_msgs = []
prev_paper_name = None
total_pages = df.groupby(df.columns[0])[df.columns[1]].max()
for _, row in df.iterrows():
paper_name = row[df.columns[0]]
page_number = row[df.columns[1]]
if paper_name != prev_paper_name:
print(f'Processing paper: {paper_name}. Total pages: {total_pages[paper_name]}')
prev_paper_name = paper_name
context = row['content']
user_prompt = """I will provide a context. Determine if the section contains a comprehensive MOF synthesis with explicit reactant quantities or solvent volumes.
Examples:
1. Context: "In a 4-mL scintillation vial, the linker H2PZVDC (91.0 mg, 0.5 mmol, 1 equiv.) was dissolved in N,N-dimethylformamide (DMF) (0.6 mL) upon sonication."
Answer: Yes
2. Context: "Synthesis and Characterization of MOFs, Abbreviations, and General Procedures."
Answer: No
3. Context: "The design and synthesis of metal-organic frameworks (MOFs) has yielded a large number of structures"
Answer: No
Respond with only "Yes" or "No" based on the following context:
""" + context
attempts = 3
while attempts > 0:
try:
response = client.chat.completions.create(
model="llama-3.1-70b-versatile", # or another available Groq model
messages=[
{"role": "system", "content": "You are a helpful assistant specialized in identifying MOF synthesis sections."},
{"role": "user", "content": user_prompt}
]
)
answers = response.choices[0].message.content.strip()
# Validate response
if answers in ["Yes", "No"]:
break
else:
raise ValueError("Invalid response")
except Exception as e:
attempts -= 1
if attempts > 0:
print(f"Error: {str(e)}. Retrying in 60 seconds. {attempts} attempts remaining. (model 2)")
time.sleep(60)
else:
print(f"Error: Failed to process paper {paper_name}. Skipping. (model 2)")
answers = "No"
break
response_msgs.append(answers)
df = df.copy()
df.loc[:,'classification'] = response_msgs
# Remove consecutive "No" entries
mask_no = df["classification"].str.startswith("No")
mask_surrounded_by_no = mask_no.shift(1, fill_value=False) & mask_no.shift(-1, fill_value=False)
mask_to_remove = mask_no & mask_surrounded_by_no
filtered_df = df[~mask_to_remove]
# Combine sections and process
combined_df = combine_main_SI(combine_section(filtered_df))
add_table_df = model_1(combined_df)
return add_table_df[['file name','summarized']]