Spaces:
Runtime error
Runtime error
File size: 14,871 Bytes
7697a39 c8e58cc 8c8c5db c8e58cc 7697a39 e5fd599 7697a39 c8e58cc 7697a39 c8e58cc 7697a39 c8e58cc 7697a39 c8e58cc 7697a39 c8e58cc 7697a39 c8e58cc 7697a39 c8e58cc 7697a39 c8e58cc 7697a39 c8e58cc 7697a39 c8e58cc 7697a39 c8e58cc 7697a39 c8e58cc 7697a39 c8e58cc 7697a39 c8e58cc 7697a39 88dc7bb c8e58cc 7697a39 c8e58cc 7697a39 fae8909 c8e58cc 7697a39 c8e58cc 7697a39 c8e58cc 7697a39 c8e58cc 7697a39 c8e58cc e5fd599 7697a39 ea50b28 7697a39 ea50b28 faecfdf 7697a39 c8e58cc 88dc7bb c8e58cc 8c8c5db c8e58cc 88dc7bb c8e58cc 7697a39 c8e58cc 7697a39 c8e58cc 88dc7bb c8e58cc 7697a39 cda9639 7697a39 cda9639 7697a39 cda9639 88dc7bb cda9639 faecfdf 7697a39 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 |
"""
This module provides functions for working with PDF files and URLs. It uses the urllib.request library
to download files from URLs, and the fitz library to extract text from PDF files. And GPT3 modules to generate
text completions.
"""
import urllib.request
import fitz
import re
import numpy as np
import tensorflow_hub as hub
import openai
import gradio as gr
import os
from sklearn.neighbors import NearestNeighbors
from typing import Optional, Union, IO
def download_pdf(url: str, output_path: str) -> None:
"""
Downloads a PDF file from the given URL and saves it to the specified output path.
Args:
url (str): The URL of the PDF file to be downloaded.
output_path (str): The file path where the downloaded PDF file will be saved.
Returns:
None
"""
urllib.request.urlretrieve(url, output_path)
def preprocess(text: str) -> str:
"""
Preprocesses the given text by replacing newline characters with spaces and removing extra whitespaces.
Args:
text (str): The input text to be preprocessed.
Returns:
str: The preprocessed text with newline characters replaced by spaces and extra whitespaces removed.
Example:
>>> preprocess("Hello\\n world!")
'Hello world!'
"""
text = text.replace('\n', ' ')
text = re.sub(r'\s+', ' ', text)
return text
def pdf_to_text(path: str, start_page: int = 1, end_page: Optional[int] = None) -> list[str]:
"""
Converts a PDF file to a list of text strings.
Args:
path (str): The path to the PDF file.
start_page (int): The page number to start extracting text from (default is 1).
end_page (int): Page number to stop extracting text at (default is None, which means extract text from all ]
pages)
Returns:
list: A list of text strings extracted from the PDF file.
"""
doc = fitz.open(path)
total_pages = doc.page_count
if end_page is None:
end_page = total_pages
text_list = []
for i in range(start_page-1, end_page):
text = doc.load_page(i).get_text("text")
text = preprocess(text)
text_list.append(text)
doc.close()
return text_list
def text_to_chunks(texts: list[str], word_length: int = 150, start_page: int = 1) -> list[str]:
"""
Splits a list of texts into chunks of specified length and formats them as strings.
Args:
- texts: A list of strings to be split into chunks.
- word_length: An integer representing the maximum number of words in each chunk. Default is 150.
- start_page: An integer representing the starting page number. Default is 1.
Returns:
- A list of formatted string chunks, where each chunk contains a page number, enclosed in square brackets,
followed by the chunk of text enclosed in double quotes.
Example:
>>> texts = ['This is a sample text for testing the function.', 'It should split the text into chunks of 5 words.']
>>> text_to_chunks(texts, word_length=5, start_page=3)
['[3] "This is a sample text for"', '[3] "testing the function. It should"',
'[4] "split the text into chunks of"','[4] "5 words."']
"""
text_toks = [t.split(' ') for t in texts]
chunks = []
for idx, words in enumerate(text_toks):
for i in range(0, len(words), word_length):
chunk = words[i:i+word_length]
if (i+word_length) > len(words) and (len(chunk) < word_length) and (
len(text_toks) != (idx+1)):
text_toks[idx+1] = chunk + text_toks[idx+1]
continue
chunk = ' '.join(chunk).strip()
chunk = f'[{idx+start_page}]' + ' ' + '"' + chunk + '"'
chunks.append(chunk)
return chunks
class SemanticSearch(object):
"""
This class provides functionality for semantic search.
"""
def __init__(self) -> None:
"""
Initializes an instance of the class.
Attributes:
-----------
use : tensorflow_hub.KerasLayer
A pre-trained Universal Sentence Encoder model from TensorFlow Hub.
fitted : bool
A flag indicating whether the model has been fitted to data or not.
"""
self.use = hub.load(
'https://tfhub.dev/google/universal-sentence-encoder/4')
self.fitted = False
def fit(self, data: list[str], batch: int = 1000, n_neighbors: int = 5) -> None:
"""
Fits the nearest neighbor model to the given data.
Args:
data (list[str]): A list of strings to fit the model on.
batch (int): The batch size to use when computing text embeddings. Defaults to 1000.
n_neighbors (int): The number of nearest neighbors to find for each query. Defaults to 5.
Returns:
None
"""
self.data = data
self.embeddings = self.get_text_embedding(data, batch=batch)
n_neighbors = min(n_neighbors, len(self.embeddings))
self.nn = NearestNeighbors(n_neighbors=n_neighbors)
self.nn.fit(self.embeddings)
self.fitted = True
def __call__(self, text: str, return_data: bool = True) -> Union[list[str], np.ndarray]:
"""
Finds nearest neighbors to a given text in the embedding space.
Args:
text (str): The input text to find nearest neighbors for.
return_data (bool): Whether to return the actual data points corresponding to the nearest neighbors.
If False, returns only the indices of the nearest neighbors. Defaults to True.
Returns:
Union[List[str], np.ndarray]: If return_data is True, returns a list of strings representing the
nearest neighbors. If return_data is False, returns a numpy array of shape (n_neighbors,)
containing the indices of the nearest neighbors.
"""
inp_emb = self.use([text])
neighbors = self.nn.kneighbors(inp_emb, return_distance=False)[0]
if return_data:
return [self.data[i] for i in neighbors]
else:
return neighbors
def get_text_embedding(self, texts: list[str], batch: int = 1000) -> np.ndarray:
"""
Generates embeddings for a list of texts using the Universal Sentence Encoder.
Args:
texts (List[str]): A list of strings to generate embeddings for.
batch (int): The batch size to use when generating embeddings. Defaults to 1000.
Returns:
np.ndarray: An array of shape (n_texts, embedding_size) containing the embeddings for each text.
"""
embeddings = []
for i in range(0, len(texts), batch):
text_batch = texts[i:(i+batch)]
emb_batch = self.use(text_batch)
embeddings.append(emb_batch)
embeddings = np.vstack(embeddings)
return embeddings
def load_recommender(path: str, start_page: int = 1) -> str:
"""
Loads embeddings from file if available, otherwise generates embeddings and saves them to file.
Args:
path (str): The path of the PDF file.
start_page (int): The page number to start generating embeddings from. Default is 1.
Returns:
str: A message indicating whether embeddings were loaded from file or generated and saved to file.
"""
global recommender
pdf_file = os.path.basename(path)
embeddings_file = f"{pdf_file}_{start_page}.npy"
if os.path.isfile(embeddings_file):
embeddings = np.load(embeddings_file)
recommender.embeddings = embeddings
recommender.fitted = True
return "Embeddings loaded from file"
texts = pdf_to_text(path, start_page=start_page)
chunks = text_to_chunks(texts, start_page=start_page)
recommender.fit(chunks)
np.save(embeddings_file, recommender.embeddings)
return 'Corpus Loaded.'
def generate_text(openai_key: str, prompt: str, engine: str = "text-davinci-003") -> str:
"""
Generates text using OpenAI's GPT-3 language model.
Parameters:
openai_key (str): The API key for accessing OpenAI's API.
prompt (str): The starting text prompt to generate the text from.
engine (str): The ID of the language model to use. Defaults to "text-davinci-003".
Returns:
str: The generated text based on the given prompt.
"""
openai.api_key = openai_key
completions = openai.Completion.create(
engine=engine,
prompt=prompt,
max_tokens=512,
n=1,
stop=None,
temperature=0.7,
)
message = completions.choices[0].text
return message
def generate_text2(openai_key: str, prompt: str, engine: str = "gpt-3.5-turbo-0301") -> str:
"""
Generates text using OpenAI's GPT-3 language model.
Args:
openai_key (str): The API key for accessing OpenAI's GPT-3 language model.
prompt (str): The user's prompt to generate a response to.
engine (str, optional): The name of the GPT-3 engine to use. Defaults to "gpt-3.5-turbo-0301".
Returns:
str: The generated text response from the GPT-3 language model.
"""
openai.api_key = openai_key
messages = [{'role': 'system', 'content': 'You are a helpful assistant.'},
{'role': 'user', 'content': prompt}]
completions = openai.ChatCompletion.create(
model=engine,
messages=messages,
max_tokens=512,
n=1,
stop=None,
temperature=0.7,
)
message = completions.choices[0].message['content']
return message
def generate_answer(question: str, openai_key: str) -> str:
"""
Generates an answer to the given question using OpenAI's GPT-3 language model.
Args:
question (str): The question to answer.
openai_key (str): The API key for accessing OpenAI's GPT-3 API.
Returns:
str: The generated answer to the question.
"""
topn_chunks = recommender(question)
prompt = ""
prompt += 'search results:\n\n'
for c in topn_chunks:
prompt += c + '\n\n'
prompt += "Instructions: Compose a comprehensive reply to the query using the search results given. "\
"Cite each reference using [ Page Number] notation (every result has this number at the beginning). "\
"Citation should be done at the end of each sentence. If the search results mention multiple subjects "\
"with the same name, create separate answers for each. Only include information found in the results and "\
"don't add any additional information. Make sure the answer is correct and don't output false content. "\
"If the text does not relate to the query, simply state 'Text Not Found in PDF'. Ignore outlier "\
"search results which has nothing to do with the question. Only answer what is asked. The "\
"answer should be short and concise. Answer step-by-step. \n\nQuery: {question}\nAnswer: "
prompt += f"Query: {question}\nAnswer:"
answer = generate_text(openai_key, prompt, "text-davinci-003")
return answer
def question_answer(url: str, file: IO[str], question: str, openai_key: str) -> str:
"""
Generates an answer to a given question using OpenAI's GPT-3 model.
Parameters:
-----------
url : str
The URL of a webpage to extract text from. If provided, the text will be saved as a PDF and used
as input for the model.
file : file-like object
A file object containing a PDF document to use as input for the model. If provided, the text will
be extracted from the PDF and used as input for the model.
question : str
The question to generate an answer for.
openai_key : str
An API key for accessing OpenAI's GPT-3 model.
Returns:
--------
str
The generated answer to the given question.
Raises:
-------
ValueError
If both `url` and `file` are empty or if both are provided.
If `question` is empty.
"""
if openai_key.strip() == '':
return '[ERROR]: Please enter you Open AI Key. Get your key here : https://platform.openai.com/account/api-keys'
if url.strip() == '' and file == None:
return '[ERROR]: Both URL and PDF is empty. Provide at least one.'
if url.strip() != '' and file != None:
return '[ERROR]: Both URL and PDF is provided. Please provide only one (either URL or PDF).'
if url.strip() != '':
glob_url = url
download_pdf(glob_url, 'corpus.pdf')
load_recommender('corpus.pdf')
else:
old_file_name = file.name
file_name = file.name
file_name = file_name[:-12] + file_name[-4:]
os.rename(old_file_name, file_name)
load_recommender(file_name)
if question.strip() == '':
return '[ERROR]: Question field is empty'
return generate_answer(question, openai_key)
recommender = SemanticSearch()
title = 'PDF GPT'
description = """ What is PDF GPT ?
1. The problem is that Open AI has a 4K token limit and cannot take an entire PDF file as input. Additionally,
it sometimes returns irrelevant responses due to poor embeddings. ChatGPT cannot directly talk to external data.
The solution is PDF GPT, which allows you to chat with an uploaded PDF file using GPT functionalities.
The application breaks the document into smaller chunks and generates embeddings using a powerful Deep Averaging
Network Encoder. A semantic search is performed on your query, and the top relevant chunks are used to generate a
response.
2. The returned response can even cite the page number in square brackets([]) where the information is located,
adding credibility to the responses and helping to locate pertinent information quickly. The Responses are much
better than the naive responses by Open AI."""
with gr.Blocks() as demo:
gr.Markdown(f'<center><h1>{title}</h1></center>')
gr.Markdown(description)
with gr.Row():
with gr.Group():
gr.Markdown(
'<p style="text-align:center">'
'Get your Open AI API key <a href="https://platform.openai.com/account/api-keys">here</a>'
'</p>'
)
openAI_key = gr.Textbox(label='Enter your OpenAI API key here')
url = gr.Textbox(label='Enter PDF URL here')
gr.Markdown("<center><h4>OR<h4></center>")
file = gr.File(
label='Upload your PDF/ Research Paper / Book here', file_types=['.pdf'])
question = gr.Textbox(label='Enter your question here')
btn = gr.Button(value='Submit')
btn.style(full_width=True)
with gr.Group():
answer = gr.Textbox(label='The answer to your question is :')
btn.click(question_answer, inputs=[
url, file, question, openAI_key], outputs=[answer])
demo.launch() |