File size: 12,093 Bytes
47b5933
27a118f
2d9ebba
47b5933
 
 
 
97328e1
47b5933
a137633
 
 
97328e1
47b5933
 
 
 
 
97328e1
47b5933
 
 
 
 
a137633
 
47b5933
 
 
 
 
 
 
 
 
97328e1
 
 
47b5933
97328e1
 
a137633
 
47b5933
97328e1
 
 
47b5933
a137633
97328e1
 
 
 
47b5933
a137633
47b5933
a137633
47b5933
 
 
 
 
a137633
47b5933
 
a137633
 
 
 
 
 
 
 
72a7cb8
a137633
 
26ca190
 
a137633
 
 
47b5933
a137633
47b5933
a137633
47b5933
 
a137633
47b5933
 
 
 
 
 
 
 
 
1ab72ca
 
47b5933
 
 
 
 
 
 
a137633
654f668
47b5933
 
 
a137633
47b5933
 
a137633
 
 
26ca190
a137633
 
 
 
 
47b5933
1dae95f
47b5933
 
a137633
97328e1
 
 
 
 
 
 
 
 
 
47b5933
97328e1
47b5933
a137633
97328e1
 
a137633
ee2e313
97328e1
 
47b5933
a137633
47b5933
a137633
 
 
 
 
26ca190
a137633
 
 
 
654f668
97328e1
 
 
47b5933
a137633
47b5933
a137633
47b5933
 
a137633
47b5933
 
 
a137633
47b5933
 
a137633
47b5933
 
 
 
a137633
47b5933
 
 
 
 
 
 
 
 
 
a137633
47b5933
 
 
97328e1
47b5933
 
 
 
 
 
 
 
b0cde6b
 
 
 
 
 
 
 
47b5933
97328e1
47b5933
 
 
 
 
 
 
 
 
 
 
97328e1
2d9ebba
47b5933
 
a59ab4a
47b5933
a137633
47b5933
 
 
 
 
97328e1
47b5933
 
 
 
 
7dea390
47b5933
 
a137633
0ca1136
47b5933
 
 
 
97328e1
 
 
a137633
97328e1
 
 
 
 
 
 
 
 
 
 
a137633
97328e1
 
 
 
 
 
 
 
 
47b5933
 
 
a137633
47b5933
 
 
 
a137633
 
 
47b5933
 
 
 
 
 
 
 
 
a137633
 
47b5933
a137633
 
72a7cb8
47b5933
a137633
237c691
47b5933
a137633
 
47b5933
 
a137633
47b5933
 
 
a137633
 
47b5933
 
 
 
 
 
 
b0cde6b
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
from fastapi import FastAPI, File
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from dotenv import load_dotenv
import time
import os
import requests
import bs4

from langchain_qdrant import QdrantVectorStore
from qdrant_client import QdrantClient
from qdrant_client.http.models import Distance, VectorParams
from langchain_community.document_loaders import PyPDFLoader, WebBaseLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_google_genai import GoogleGenerativeAIEmbeddings, GoogleGenerativeAI
from langchain.chains.combine_documents import create_stuff_documents_chain
from langchain_core.prompts import PromptTemplate

def chunk_document(document, chunk_size=600, chunk_overlap=80):
    """
    Divides the document into smaller, overlapping chunks for better processing efficiency.

    Args:
        document (list): A list of fetched content from document.
        chunk_size (int, optional): The maximum number of words ia a chunk. Default is 600.
        chunk_overlap (int, optional): The number of overlapping words between consecutive chunks. Default is 80.

    Returns:
        list: A list of document chunks, where each chunk is a Documentof content with the specified size and overlap.
    """
    
    text_splitter = RecursiveCharacterTextSplitter(chunk_size=chunk_size, chunk_overlap=chunk_overlap)
    chunks = text_splitter.split_documents(document)
    return chunks

def chunk_article(document, chunk_size=600, chunk_overlap=80):
    """
    Divides the article text into smaller, overlapping chunks for better processing efficiency.

    Args:
        extracted_text (str): The extracted text content from the article URL.
        chunk_size (int, optional): The maximum number of words in a chunk. Default is 600.
        chunk_overlap (int, optional): The number of overlapping words between consecutive chunks. Default is 80.

    Returns:
        list: A list of article chunks, where each chunk is a Documentof content with the specified size and overlap.
    """
    
    splitter = RecursiveCharacterTextSplitter(chunk_size = chunk_size, chunk_overlap = chunk_overlap)

    splitted_docs = splitter.split_documents(document)

    return splitted_docs

def creating_qdrant_index(embeddings):
    """
    Creates a Qdrant index using the provided embedding model.

    Args:
        embedding (object): The embedding model or function used to generate vector embeddings.

    Returns:
        QdrantVectorStore: An instance of Qdrant index where the vectors can be processed.
    """
    
    # client = QdrantClient(url="http://localhost:6333")

    # client.create_collection(
    #     collection_name="rag-documents",
    #     vectors_config=VectorParams(size=768, distance=Distance.COSINE),
    # )

    vector_store = QdrantVectorStore.from_existing_collection(
        url="https://a2790d6c-f701-4a62-a57c-81d8fb4558f8.us-east-1-0.aws.cloud.qdrant.io",
        collection_name="rag-documents",
        embedding=embeddings,
        api_key=QDRANT_CLOUD_KEY,
        prefer_grpc=False
    )

    return vector_store

def uploading_document_to_database(directory):
    """
    Uploads a document from a specified directory to the Qdrant index after processing and chunking the content.

    Args:
        directory (str): The file path of the PDF document that will be uploaded to Qdrant.

    Returns:
        None: This function does not return any value.
    """
    print("Loading PDF : ", directory)
    pdf_loader = PyPDFLoader(directory)
    document = pdf_loader.load()

    # Replacing newline characters with spaces
    for chunk in document:
        chunk.page_content = chunk.page_content.replace('\n', ' ')
    
    # Dividing document content into chunks
    chunked_data = chunk_document(document)

    print("Deleting file")
    try:
        # Deleting all existing data on Pinecone index
        qdrant_index.delete(delete_all=True)
        time.sleep(5)
    except:
        print("Namespace is already empty")
    
    print("Uploading File to Database")
    
    # Uploading the chunked data to Pinecone index
    qdrant_index.from_documents(
        chunked_data,
        embeddings,
        prefer_grpc=False, 
        collection_name="rag-documents",
    )

    print("Document Uploaded to Database")
    time.sleep(5)
    prompt = "What is the Title of the document and a small description of the content."
    description = response_generator(query = prompt, profession="Student")
    return description

def uploading_article_to_database(url):
    strainer = bs4.SoupStrainer(["article", "main"])

    loader = WebBaseLoader(
        web_path = url,
        bs_kwargs = {"parse_only": strainer},
    )

    document = loader.load()
    document[0].page_content = document[0].page_content.replace("\n\n\n", " ").strip()
    document[0].page_content = document[0].page_content.replace("\n\n", " ").strip()

    chunked_data = chunk_article(document)

    print("Deleting previous data")
    try:
        # Deleting all existing data on Pinecone index
        qdrant_index.delete(delete_all=True)
        time.sleep(5)
    except:
        print("Namespace is already empty")
    
    print("Uploading Article to Database")
    
    # Uploading the chunked data to Qdrant index
    qdrant_index.from_documents(
        chunked_data,
        embeddings,
        url = url, 
        prefer_grpc=False, 
        collection_name="rag-documents",
    )

    print("Article Uploaded to Database")
    time.sleep(15)
    prompt = "What is the Title of the document and a small description of the content."
    description = response_generator(query = prompt, profession="Student")
    return description

def retrieve_response_from_database(query, k=5):
    """
    Retrieves the most similar responses from the Qdrant index based on the given query.

    Args:
        query (str): The input query used to search the Qdrant index for vectors.
        k (int, optional): Indicates top results to choose. Default is 5.

    Returns:
        list: A list of results containing the most similar vectors from the Qdrant index.
    """
    
    results = qdrant_index.similarity_search(query, k=k)
    return results

def response_generator(query, profession):
    """
    Generates a response to the given query by retrieving relevant information from the Qdrant index and invoking 
    a processing chain with llm.

    Args:
        query (str): The user's input or question that will be used to retrieve relevant information and generate a response.

    Returns:
        str: The generated response to the query, either based on the retrieved information or an error messageif the process fails.
    """
    
    try:
        results = retrieve_response_from_database(query)
        print("results", results)

        # Generating a response by invoking the chain with retrieved content and the original query
        answer = chain.invoke(input={"profession": profession, "context": results, "user_query": query})
    except Exception as e:
        # Returning an error message if any exception occurs
        answer = f"Sorry, I am unable to find the answer to your query. Please try again later. The error is {e}"
    
    return answer

app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

@app.get("/get_response")
def root(query: str, profession: str):
    """
    FastAPI endpoint to handle GET requests and return a generated response for a user's query.

    Args:
        query (str): The query string input from the user, passed as a path parameter in the API request.

    Returns:
        dict: A dictionary containing the response generated from the query.
    """
    
    print("User_query : " + query)
    answer = response_generator(query, profession)
    return JSONResponse(content={"answer": answer})

@app.post("/upload_document")
def upload_document(file_bytes: bytes = File(...)):
    """
    FastAPI endpoint to handle POST requests for uploading a document to the Qdrant index.

    Args:
        file_bytes (bytes): The byte data of the document file that will be uploaded to the Pinecone index.

    Returns:
        dict: A dictionary containing the description of the document uploaded.
    """
    
    try:

        # Save the uploaded file
        with open("/tmp/document.pdf", "wb") as f:
            f.write(file_bytes)

        description = uploading_document_to_database("/tmp/document.pdf")
        response = requests.post("http://0.0.0.0:8080/send_desc", json={"description": description})
        return {"status": description}
    except Exception as e:
        return {"status": f"Error uploading file: {e}"}

@app.post("/upload_article")
def upload_article(url: str):
    """
    FastAPI endpoint to handle POST requests for uploading a web article to the Qdrant index.

    Args:
        url (string): The string value that contains the url of the web article.

    Returns:
        dict: A dictionary containing the description of the article uploaded."""

    try:
        print("URL to server : ", url)

        #Uploading process of article and getting description
        description = uploading_article_to_database(url)

        # Providing the description to the AI agents
        response = requests.post("http://0.0.0.0:8080/send_desc", json={"description": description})
        print("type(description) : ", type(description))

        # Returning the description of the article
        return {"status": description}
    except Exception as e:
        return {"status": f"Error uploading file: {e}"}

if __name__ == "__main__":
    """
    Initializes the FastAPI server, loads environment variables, creates an embedding model and Qdrant index, 
    uploads a document for processing, and sets up a language model for generating responses.

    This block of code performs the following tasks:
    - Loads environment variables.
    - Initializes the embedding model for Qdrant chunking and retrieval.
    - Creates a Qdrant index to store document embeddings.
    - Uploads a specific PDF document to the Qdrant index for later query-based retrieval.
    - Sets up a language model (LLM) for generating human-like responses.
    - Defines the system prompt and response behavior for the assistant.
    - Sets up a chain that combines document retrieval with response generation.
    - Starts the FastAPI server on host `0.0.0.0` at port 8000.
    """

    # Loading environment variables from .env file
    load_dotenv()

    QDRANT_CLOUD_KEY = os.getenv(key="QDRANT_CLOUD_KEY")

    # Initializing embedding model for creating document vectors
    embeddings = GoogleGenerativeAIEmbeddings(model="models/text-embedding-004")

    url = "https://a2790d6c-f701-4a62-a57c-81d8fb4558f8.us-east-1-0.aws.cloud.qdrant.io"

    # Qdrant index name for storing document embeddings
    index_name = "rag-chatbot"

    # Creating Qdrant index using the embedding model
    qdrant_index = creating_qdrant_index(embeddings)

    # Initializing the LLM with the 'gemini-1.5-flash' model and a specified temperature for response generation
    llm = GoogleGenerativeAI(model="gemini-1.5-flash", temperature=0.6)

    # Creating a prompt template for generating responses based on retrieved content and human input
    prompt_template = PromptTemplate(
        template="I am {profession}. You have to provide a good information regarding my query. This is the information from my document : {context}. Here is my query for you: {user_query}. Answer in a proper markdown format.",
        input_variables=["profession", "context", "user_query"]
    )

    # Setting up the document processing chain for response generation based on retrieved documents
    chain = create_stuff_documents_chain(llm, prompt_template, document_variable_name="context")

    # Starting the FastAPI server with Uvicorn, accessible at 0.0.0.0 on port 8000
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)