mafzaal commited on
Commit
37c6d5c
·
0 Parent(s):

Refactor code structure for improved readability and maintainability

Browse files
Files changed (12) hide show
  1. .gitattributes +35 -0
  2. .gitignore +6 -0
  3. Dockerfile +31 -0
  4. README.md +83 -0
  5. app.py +15 -0
  6. chainlit.md +3 -0
  7. config.py +18 -0
  8. handlers/chainlit_handlers.py +98 -0
  9. models/rag.py +69 -0
  10. pyproject.toml +22 -0
  11. utils/file_processor.py +59 -0
  12. uv.lock +0 -0
.gitattributes ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ .chainlit/
3
+ .venv/
4
+ .env
5
+ .chainlit/
6
+ .files/
Dockerfile ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ # Get a distribution that has uv already installed
3
+ FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim
4
+
5
+ # Add user - this is the user that will run the app
6
+ # If you do not set user, the app will run as root (undesirable)
7
+ RUN useradd -m -u 1000 user
8
+ USER user
9
+
10
+ # Set the home directory and path
11
+ ENV HOME=/home/user \
12
+ PATH=/home/user/.local/bin:$PATH
13
+
14
+ ENV UVICORN_WS_PROTOCOL=websockets
15
+
16
+
17
+ # Set the working directory
18
+ WORKDIR $HOME/app
19
+
20
+ # Copy the app to the container
21
+ COPY --chown=user . $HOME/app
22
+
23
+ # Install the dependencies
24
+ # RUN uv sync --frozen
25
+ RUN uv sync
26
+
27
+ # Expose the port
28
+ EXPOSE 7860
29
+
30
+ # Run the app
31
+ CMD ["uv", "run", "chainlit", "run", "app.py", "--host", "0.0.0.0", "--port", "7860"]
README.md ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Research Agent
3
+ emoji: 📉
4
+ colorFrom: blue
5
+ colorTo: purple
6
+ sdk: docker
7
+ pinned: false
8
+ license: apache-2.0
9
+ ---
10
+
11
+ # Research Agent
12
+
13
+ A document-based Q&A application built with LangChain and Chainlit that allows users to upload documents and ask questions about their content.
14
+
15
+ ## Features
16
+
17
+ - Upload PDF or text documents
18
+ - Ask questions about the uploaded documents
19
+ - Get AI-generated answers based on the document content
20
+ - Streaming responses for better user experience
21
+
22
+ ## Technology Stack
23
+
24
+ - **LangChain**: Framework for developing applications powered by language models
25
+ - **Chainlit**: Frontend for creating chat-based applications
26
+ - **Qdrant**: Vector database for storing and retrieving document embeddings
27
+ - **OpenAI**: Provides the language model and embeddings
28
+
29
+ ## How It Works
30
+
31
+ 1. User uploads a PDF or text document
32
+ 2. The application processes the document:
33
+ - Splits it into manageable chunks
34
+ - Creates embeddings using OpenAI
35
+ - Stores these embeddings in Qdrant vector database
36
+ 3. User asks questions about the document
37
+ 4. The application:
38
+ - Retrieves relevant chunks using semantic search
39
+ - Uses a Retrieval-Augmented Generation (RAG) pipeline to generate answers
40
+ - Returns streaming responses to the user
41
+
42
+ ## Getting Started
43
+
44
+ ### Prerequisites
45
+
46
+ - Python 3.8+
47
+ - OpenAI API key
48
+
49
+ ### Installation
50
+
51
+ ```bash
52
+ # Clone the repository
53
+ git clone <repository-url>
54
+ cd AIE6-ResearchAgent
55
+
56
+ # Install dependencies
57
+ pip install -r requirements.txt
58
+ # Or using uv
59
+ uv add langchain langchain-openai langchain-community langchain-core langchain-text-splitters langchain-qdrant qdrant-client chainlit
60
+ ```
61
+
62
+ ### Running the Application
63
+
64
+ ```bash
65
+ # Set your OpenAI API key
66
+ export OPENAI_API_KEY=your-api-key
67
+
68
+ # Start the application
69
+ chainlit run app.py
70
+ ```
71
+
72
+ ## Docker Deployment
73
+
74
+ The application can also be deployed using Docker:
75
+
76
+ ```bash
77
+ docker build -t research-agent .
78
+ docker run -p 7860:7860 -e OPENAI_API_KEY=your-api-key research-agent
79
+ ```
80
+
81
+ ## License
82
+
83
+ This project is licensed under the Apache License 2.0 - see the LICENSE file for details.
app.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Main entry point for the Research Agent application.
3
+
4
+ This file imports the necessary components from other modules and
5
+ serves as the entry point for the Chainlit application.
6
+ """
7
+
8
+ # Import the handlers to register Chainlit event handlers
9
+ from handlers.chainlit_handlers import on_chat_start, main
10
+
11
+ # The Chainlit application will automatically
12
+ # discover and use the imported event handlers
13
+
14
+ if __name__ == "__main__":
15
+ print("Research Agent started. Access the web interface to interact.")
chainlit.md ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # Welcome to Chat with Your Text/PDF File
2
+
3
+ With this application, you can chat with an uploaded text/PDFfile that is smaller than 2MB!
config.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Configuration settings for the Research Agent application.
3
+ """
4
+
5
+ # System template for RAG
6
+ SYSTEM_TEMPLATE = """Use the following context to answer a user's question.
7
+ If you cannot find the answer in the context, say you don't know the answer."""
8
+
9
+ # Text splitter configurations
10
+ CHUNK_SIZE = 1000
11
+ CHUNK_OVERLAP = 200
12
+ SEPARATORS = ["\n\n", "\n", " ", ""]
13
+
14
+ # Retrieval configurations
15
+ NUM_RETRIEVAL_RESULTS = 4
16
+
17
+ # Vector database configurations
18
+ VECTOR_DIMENSION = 1536 # For OpenAI embeddings
handlers/chainlit_handlers.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Chainlit event handlers for the Research Agent.
3
+ """
4
+ import os
5
+ import chainlit as cl
6
+ from langchain_openai import ChatOpenAI, OpenAIEmbeddings
7
+ from langchain_qdrant import QdrantVectorStore
8
+ from qdrant_client import QdrantClient
9
+ from qdrant_client.models import Distance, VectorParams
10
+
11
+ from utils.file_processor import process_file
12
+ from models.rag import LangChainRAG
13
+ import config
14
+
15
+ @cl.on_chat_start
16
+ async def on_chat_start():
17
+ """
18
+ Handler for chat start event. Prompts user to upload a file
19
+ and initializes the RAG system.
20
+ """
21
+ files = None
22
+
23
+ # Wait for the user to upload a file
24
+ while files == None:
25
+ files = await cl.AskFileMessage(
26
+ content="Please upload a Text or PDF file to begin!",
27
+ accept=["text/plain", "application/pdf"],
28
+ max_size_mb=2,
29
+ timeout=180,
30
+ ).send()
31
+
32
+ file = files[0]
33
+
34
+ msg = cl.Message(content=f"Processing `{file.name}`...")
35
+ await msg.send()
36
+
37
+ # Load and process the file
38
+ texts = process_file(file)
39
+ print(f"Processing {len(texts)} text chunks")
40
+
41
+ # Initialize embeddings
42
+ embeddings = OpenAIEmbeddings()
43
+
44
+ # Create a unique collection name based on the file name
45
+ collection_name = f"collection_{file.name.replace('.', '_')}_{os.urandom(4).hex()}"
46
+
47
+ # Initialize Qdrant client (using in-memory storage)
48
+ client = QdrantClient(":memory:")
49
+
50
+ # Create collection with proper vector dimensions for OpenAI embeddings
51
+ client.create_collection(
52
+ collection_name=collection_name,
53
+ vectors_config=VectorParams(size=config.VECTOR_DIMENSION, distance=Distance.COSINE)
54
+ )
55
+
56
+ # Create vector store with QdrantVectorStore
57
+ vector_store = QdrantVectorStore(
58
+ client=client,
59
+ collection_name=collection_name,
60
+ embedding=embeddings
61
+ )
62
+
63
+ # Add documents to the vector store
64
+ vector_store.add_documents(texts)
65
+
66
+ # Create a retriever
67
+ retriever = vector_store.as_retriever(search_kwargs={"k": config.NUM_RETRIEVAL_RESULTS})
68
+
69
+ # Initialize language model
70
+ llm = ChatOpenAI(streaming=True)
71
+
72
+ # Create RAG chain
73
+ rag_chain = LangChainRAG(retriever=retriever, llm=llm)
74
+
75
+ # Let the user know that the system is ready
76
+ msg.content = f"Processing `{file.name}` done. You can now ask questions!"
77
+ await msg.update()
78
+
79
+ cl.user_session.set("chain", rag_chain)
80
+
81
+ @cl.on_message
82
+ async def main(message):
83
+ """
84
+ Handler for user messages. Processes the query through the RAG chain
85
+ and streams the response back to the user.
86
+
87
+ Args:
88
+ message: The user's message
89
+ """
90
+ chain = cl.user_session.get("chain")
91
+
92
+ msg = cl.Message(content="")
93
+ result = await chain.arun_pipeline(message.content)
94
+
95
+ async for stream_resp in result["response"]:
96
+ await msg.stream_token(stream_resp)
97
+
98
+ await msg.send()
models/rag.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ RAG (Retrieval Augmented Generation) model implementation.
3
+ """
4
+ from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
5
+ from langchain_core.output_parsers import StrOutputParser
6
+ from langchain_core.runnables import RunnablePassthrough
7
+
8
+ import config
9
+
10
+ # Create prompt template
11
+ prompt = ChatPromptTemplate.from_messages([
12
+ ("system", config.SYSTEM_TEMPLATE),
13
+ MessagesPlaceholder(variable_name="chat_history"),
14
+ ("human", "{question}"),
15
+ ("human", "Context: {context}")
16
+ ])
17
+
18
+ class LangChainRAG:
19
+ """
20
+ RAG implementation using LangChain components.
21
+ """
22
+ def __init__(self, retriever, llm):
23
+ """
24
+ Initialize the RAG model.
25
+
26
+ Args:
27
+ retriever: Document retriever component
28
+ llm: Language model for generation
29
+ """
30
+ self.retriever = retriever
31
+ self.llm = llm
32
+ self.chain = self._create_chain()
33
+
34
+ def _create_chain(self):
35
+ """
36
+ Create the RAG chain.
37
+
38
+ Returns:
39
+ A runnable chain that processes user queries
40
+ """
41
+ # Define the RAG chain
42
+ rag_chain = (
43
+ {"context": self.retriever, "question": RunnablePassthrough(), "chat_history": lambda _: []}
44
+ | prompt
45
+ | self.llm
46
+ | StrOutputParser()
47
+ )
48
+ return rag_chain
49
+
50
+ async def arun_pipeline(self, user_query: str):
51
+ """
52
+ Run the RAG pipeline with the user query.
53
+
54
+ Args:
55
+ user_query: User's question
56
+
57
+ Returns:
58
+ Dict containing the response generator and context
59
+ """
60
+ # Get relevant documents for context
61
+ docs = self.retriever.invoke(user_query)
62
+ context_list = [(doc.page_content, doc.metadata) for doc in docs]
63
+
64
+ # Create async generator for streaming
65
+ async def generate_response():
66
+ async for chunk in self.chain.astream(user_query):
67
+ yield chunk
68
+
69
+ return {"response": generate_response(), "context": context_list}
pyproject.toml ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "aie5-deploypythonicrag"
3
+ version = "0.1.0"
4
+ description = "Simple Pythonic RAG App"
5
+ readme = "README.md"
6
+ requires-python = ">=3.13"
7
+ dependencies = [
8
+ "chainlit==2.0.4",
9
+ "langchain>=0.3.23",
10
+ "langchain-community>=0.3.21",
11
+ "langchain-core>=0.3.54",
12
+ "langchain-openai>=0.3.8",
13
+ "langchain-qdrant>=0.2.0",
14
+ "langchain-text-splitters>=0.3.8",
15
+ "numpy==2.2.2",
16
+ "openai==1.59.9",
17
+ "pydantic==2.10.1",
18
+ "pypdf>=5.4.0",
19
+ "pypdf2==3.0.1",
20
+ "qdrant-client>=1.13.3",
21
+ "websockets==14.2",
22
+ ]
utils/file_processor.py ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Utilities for processing uploaded files.
3
+ """
4
+ import os
5
+ import tempfile
6
+ import shutil
7
+ from typing import List
8
+
9
+ from langchain_text_splitters import RecursiveCharacterTextSplitter
10
+ from langchain_community.document_loaders import PyPDFLoader, TextLoader
11
+ from chainlit.types import AskFileResponse
12
+
13
+ import config
14
+
15
+ # Initialize text splitter
16
+ text_splitter = RecursiveCharacterTextSplitter(
17
+ chunk_size=config.CHUNK_SIZE,
18
+ chunk_overlap=config.CHUNK_OVERLAP,
19
+ length_function=len,
20
+ is_separator_regex=False,
21
+ separators=config.SEPARATORS
22
+ )
23
+
24
+ def process_file(file: AskFileResponse):
25
+ """
26
+ Process an uploaded file and split it into text chunks.
27
+
28
+ Args:
29
+ file: The uploaded file response from Chainlit
30
+
31
+ Returns:
32
+ List of document chunks
33
+ """
34
+ print(f"Processing file: {file.name}")
35
+
36
+ # Create a temporary file with the correct extension
37
+ suffix = f".{file.name.split('.')[-1]}"
38
+ with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as temp_file:
39
+ # Copy the uploaded file content to the temporary file
40
+ shutil.copyfile(file.path, temp_file.name)
41
+ print(f"Created temporary file at: {temp_file.name}")
42
+
43
+ try:
44
+ # Create appropriate loader
45
+ if file.name.lower().endswith('.pdf'):
46
+ loader = PyPDFLoader(temp_file.name)
47
+ else:
48
+ loader = TextLoader(temp_file.name)
49
+
50
+ # Load and process the documents
51
+ documents = loader.load()
52
+ texts = text_splitter.split_documents(documents)
53
+ return texts
54
+ finally:
55
+ # Clean up the temporary file
56
+ try:
57
+ os.unlink(temp_file.name)
58
+ except Exception as e:
59
+ print(f"Error cleaning up temporary file: {e}")
uv.lock ADDED
The diff for this file is too large to render. See raw diff