kamkol commited on
Commit
b06d945
·
1 Parent(s): f2cf2a2

Clean repository setup with PDF downloading

Browse files
Files changed (7) hide show
  1. .gitignore +5 -0
  2. Dockerfile +47 -0
  3. app.py +142 -0
  4. chainlit.md +3 -0
  5. download_pdfs.py +134 -0
  6. preprocess.py +65 -0
  7. pyproject.toml +14 -0
.gitignore ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ data/*
2
+ __pycache__/
3
+ *.pyc
4
+ .env
5
+ data/preprocessed_data.pkl
Dockerfile ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Get a distribution that has uv already installed
2
+ FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim
3
+
4
+ # Add user - this is the user that will run the app
5
+ # If you do not set user, the app will run as root (undesirable)
6
+ RUN useradd -m -u 1000 user
7
+ USER user
8
+
9
+ # Set the home directory and path
10
+ ENV HOME=/home/user \
11
+ PATH=/home/user/.local/bin:$PATH
12
+
13
+ ENV UVICORN_WS_PROTOCOL=websockets
14
+
15
+
16
+ # Set the working directory
17
+ WORKDIR $HOME/app
18
+
19
+ # Copy the app to the container
20
+ COPY --chown=user . $HOME/app
21
+
22
+ # Install the dependencies
23
+ # RUN uv sync --frozen
24
+ RUN uv sync
25
+
26
+ # Create data directory if it doesn't exist
27
+ RUN mkdir -p $HOME/app/data
28
+
29
+ # Install additional dependencies for downloading PDFs
30
+ RUN uv pip install huggingface_hub datasets
31
+
32
+ # Download PDFs from Hugging Face dataset
33
+ # This requires HF_TOKEN to be set as a build secret in Hugging Face Spaces
34
+ ARG HF_TOKEN
35
+ RUN HF_TOKEN=${HF_TOKEN} huggingface-cli login --token ${HF_TOKEN}
36
+ RUN python download_pdfs.py
37
+
38
+ # Run preprocessing to generate the embeddings
39
+ # Note: This requires the OPENAI_API_KEY environment variable to be set during build
40
+ # For Hugging Face, you'll need to use their build secrets feature
41
+ RUN OPENAI_API_KEY=${OPENAI_API_KEY} uv run python preprocess.py
42
+
43
+ # Expose the port
44
+ EXPOSE 7860
45
+
46
+ # Run the app
47
+ CMD ["uv", "run", "chainlit", "run", "app.py", "--host", "0.0.0.0", "--port", "7860"]
app.py ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import pickle
3
+ import time
4
+ from typing import List, Dict, Any
5
+ from chainlit.types import AskFileResponse
6
+ from aimakerspace.text_utils import CharacterTextSplitter, TextFileLoader, PDFLoader
7
+ from aimakerspace.openai_utils.prompts import (
8
+ UserRolePrompt,
9
+ SystemRolePrompt,
10
+ AssistantRolePrompt,
11
+ )
12
+ from aimakerspace.openai_utils.embedding import EmbeddingModel
13
+ from aimakerspace.vectordatabase import VectorDatabase
14
+ from aimakerspace.openai_utils.chatmodel import ChatOpenAI
15
+ import chainlit as cl
16
+
17
+ system_template = """\
18
+ Use the following context to answer a users question. If you cannot find the answer in the context, say you don't know the answer."""
19
+ system_role_prompt = SystemRolePrompt(system_template)
20
+
21
+ user_prompt_template = """\
22
+ Context:
23
+ {context}
24
+
25
+ Question:
26
+ {question}
27
+ """
28
+ user_role_prompt = UserRolePrompt(user_prompt_template)
29
+
30
+ class RetrievalAugmentedQAPipeline:
31
+ def __init__(self, llm: ChatOpenAI(), vector_db_retriever: VectorDatabase, metadata: List[Dict[str, Any]] = None) -> None:
32
+ self.llm = llm
33
+ self.vector_db_retriever = vector_db_retriever
34
+ self.metadata = metadata or []
35
+ self.text_to_metadata = {}
36
+
37
+ # Create lookup for text to metadata
38
+ if metadata:
39
+ texts = [key for key in self.vector_db_retriever.vectors.keys()]
40
+ for i, text in enumerate(texts):
41
+ if i < len(metadata):
42
+ self.text_to_metadata[text] = metadata[i]
43
+
44
+ async def arun_pipeline(self, user_query: str):
45
+ context_list = self.vector_db_retriever.search_by_text(user_query, k=4)
46
+
47
+ context_prompt = ""
48
+ sources = []
49
+
50
+ for context in context_list:
51
+ text = context[0]
52
+ context_prompt += text + "\n"
53
+
54
+ # Get metadata for this text if available
55
+ if text in self.text_to_metadata:
56
+ sources.append(self.text_to_metadata[text])
57
+ else:
58
+ sources.append({"filename": "unknown", "page": "unknown"})
59
+
60
+ formatted_system_prompt = system_role_prompt.create_message()
61
+
62
+ formatted_user_prompt = user_role_prompt.create_message(question=user_query, context=context_prompt)
63
+
64
+ async def generate_response():
65
+ async for chunk in self.llm.astream([formatted_system_prompt, formatted_user_prompt]):
66
+ yield chunk
67
+
68
+ return {"response": generate_response(), "sources": sources}
69
+
70
+ text_splitter = CharacterTextSplitter()
71
+
72
+ def load_preprocessed_data():
73
+ # Check if preprocessed data exists
74
+ if not os.path.exists('data/preprocessed_data.pkl'):
75
+ raise FileNotFoundError("Preprocessed data not found. Please run the preprocess.py script first.")
76
+
77
+ # Load the pre-processed data
78
+ with open('data/preprocessed_data.pkl', 'rb') as f:
79
+ data = pickle.load(f)
80
+
81
+ # Create a new vector database
82
+ vector_db = VectorDatabase()
83
+
84
+ # Directly populate the vectors dictionary
85
+ for key, vector in data['vectors'].items():
86
+ vector_db.insert(key, vector)
87
+
88
+ # Get metadata if available
89
+ metadata = data.get('metadata', [])
90
+
91
+ return vector_db, metadata
92
+
93
+ @cl.on_chat_start
94
+ async def on_chat_start():
95
+ # Send welcome message
96
+ msg = cl.Message(content="Loading knowledge base from pre-processed PDF documents...")
97
+ await msg.send()
98
+
99
+ try:
100
+ # Load pre-processed data
101
+ start_time = time.time()
102
+ vector_db, metadata = load_preprocessed_data()
103
+ load_time = time.time() - start_time
104
+ print(f"Loaded vector database in {load_time:.2f} seconds")
105
+
106
+ chat_openai = ChatOpenAI()
107
+
108
+ # Create chain
109
+ retrieval_augmented_qa_pipeline = RetrievalAugmentedQAPipeline(
110
+ vector_db_retriever=vector_db,
111
+ llm=chat_openai,
112
+ metadata=metadata
113
+ )
114
+
115
+ # Let the user know that the system is ready
116
+ msg.content = "Knowledge base loaded! You can now ask questions about AB Testing. We'll use material written by Ronny Kohavi to answer your questions!"
117
+ await msg.update()
118
+
119
+ cl.user_session.set("chain", retrieval_augmented_qa_pipeline)
120
+
121
+ except Exception as e:
122
+ msg.content = f"Error loading knowledge base: {str(e)}"
123
+ await msg.update()
124
+ raise e
125
+
126
+ @cl.on_message
127
+ async def main(message):
128
+ chain = cl.user_session.get("chain")
129
+
130
+ msg = cl.Message(content="")
131
+ result = await chain.arun_pipeline(message.content)
132
+
133
+ async for stream_resp in result["response"]:
134
+ await msg.stream_token(stream_resp)
135
+
136
+ # Add source information after the response
137
+ sources_text = "\n\n**Sources:**"
138
+ for i, source in enumerate(result["sources"]):
139
+ sources_text += f"\n- {source['filename']} (Page {source['page']})"
140
+
141
+ await msg.stream_token(sources_text)
142
+ await msg.send()
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!
download_pdfs.py ADDED
@@ -0,0 +1,134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datasets import load_dataset
2
+ import os
3
+ from huggingface_hub import hf_hub_download
4
+ import logging
5
+
6
+ # Configure logging
7
+ logging.basicConfig(level=logging.INFO)
8
+ logger = logging.getLogger(__name__)
9
+
10
+ def download_pdfs():
11
+ """
12
+ Download PDF files from the Hugging Face dataset.
13
+ """
14
+ logger.info("Creating data directory if it doesn't exist")
15
+ os.makedirs("data", exist_ok=True)
16
+
17
+ try:
18
+ logger.info("Loading the dataset from kamkol/ab_testing_pdfs")
19
+ # Try to load the dataset first
20
+ dataset = load_dataset("kamkol/ab_testing_pdfs", use_auth_token=True)
21
+
22
+ # Check if we have files in the dataset
23
+ if 'train' in dataset and len(dataset['train']) > 0:
24
+ logger.info(f"Found {len(dataset['train'])} files in dataset")
25
+
26
+ # Handle dataset format that uses binary field
27
+ if 'binary' in dataset['train'].features:
28
+ for i, item in enumerate(dataset['train']):
29
+ filename = item["filename"] if "filename" in item else f"document_{i}.pdf"
30
+ with open(f"data/{filename}", "wb") as f:
31
+ f.write(item["binary"])
32
+ logger.info(f"Downloaded: {filename}")
33
+
34
+ # Alternative approach for direct file access
35
+ else:
36
+ # List all PDF files in the repository
37
+ logger.info("Dataset doesn't have binary field, trying direct file download")
38
+ for i, item in enumerate(dataset['train']):
39
+ # Get filename from the dataset if available
40
+ if 'filename' in item:
41
+ filename = item['filename']
42
+ else:
43
+ logger.warning(f"No filename found for item {i}, using default")
44
+ filename = f"document_{i}.pdf"
45
+
46
+ # Download the file
47
+ try:
48
+ file_path = hf_hub_download(
49
+ repo_id="kamkol/ab_testing_pdfs",
50
+ filename=filename,
51
+ repo_type="dataset",
52
+ use_auth_token=True
53
+ )
54
+ # Copy to data directory
55
+ with open(file_path, 'rb') as src, open(f"data/{filename}", 'wb') as dst:
56
+ dst.write(src.read())
57
+ logger.info(f"Downloaded: {filename}")
58
+ except Exception as e:
59
+ logger.error(f"Error downloading {filename}: {str(e)}")
60
+
61
+ else:
62
+ # Fall back to direct file download from the repository
63
+ logger.info("No files found in dataset train split, trying direct repository access")
64
+
65
+ # List of PDF files to download
66
+ pdf_files = [
67
+ "trustworthy-online-controlled-experiments.pdf",
68
+ "2012-controllable-experimentation-paper.pdf",
69
+ "2011-Bing-Flight-KDD-final.pdf",
70
+ "best-practices-long.pdf",
71
+ "kohavi-dmkd-published-version.pdf",
72
+ "online-experimentation-at-microsoft.pdf",
73
+ "p609-kohavi.pdf",
74
+ "TechReport-2009-4.pdf",
75
+ "Seven-Pitfalls-to-Avoid.pdf",
76
+ "KDD-2009-ExP-Design-Pitfalls.pdf",
77
+ "wsdm-4page-paper.pdf",
78
+ "expAIsterdam-PValue.pdf",
79
+ "Interleaving-www11.pdf"
80
+ ]
81
+
82
+ for filename in pdf_files:
83
+ try:
84
+ file_path = hf_hub_download(
85
+ repo_id="kamkol/ab_testing_pdfs",
86
+ filename=filename,
87
+ repo_type="dataset",
88
+ use_auth_token=True
89
+ )
90
+ # Copy to data directory
91
+ with open(file_path, 'rb') as src, open(f"data/{filename}", 'wb') as dst:
92
+ dst.write(src.read())
93
+ logger.info(f"Downloaded: {filename}")
94
+ except Exception as e:
95
+ logger.error(f"Error downloading {filename}: {str(e)}")
96
+
97
+ except Exception as e:
98
+ logger.error(f"Error loading dataset: {str(e)}")
99
+ logger.info("Falling back to direct file download")
100
+
101
+ # List of PDF files to download directly from the hub
102
+ pdf_files = [
103
+ "trustworthy-online-controlled-experiments.pdf",
104
+ "2012-controllable-experimentation-paper.pdf",
105
+ "2011-Bing-Flight-KDD-final.pdf",
106
+ "best-practices-long.pdf",
107
+ "kohavi-dmkd-published-version.pdf",
108
+ "online-experimentation-at-microsoft.pdf",
109
+ "p609-kohavi.pdf",
110
+ "TechReport-2009-4.pdf",
111
+ "Seven-Pitfalls-to-Avoid.pdf",
112
+ "KDD-2009-ExP-Design-Pitfalls.pdf",
113
+ "wsdm-4page-paper.pdf",
114
+ "expAIsterdam-PValue.pdf",
115
+ "Interleaving-www11.pdf"
116
+ ]
117
+
118
+ for filename in pdf_files:
119
+ try:
120
+ file_path = hf_hub_download(
121
+ repo_id="kamkol/ab_testing_pdfs",
122
+ filename=filename,
123
+ repo_type="dataset",
124
+ use_auth_token=True
125
+ )
126
+ # Copy to data directory
127
+ with open(file_path, 'rb') as src, open(f"data/{filename}", 'wb') as dst:
128
+ dst.write(src.read())
129
+ logger.info(f"Downloaded: {filename}")
130
+ except Exception as e:
131
+ logger.error(f"Error downloading {filename}: {str(e)}")
132
+
133
+ if __name__ == "__main__":
134
+ download_pdfs()
preprocess.py ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import pickle
3
+ import numpy as np
4
+ from aimakerspace.text_utils import CharacterTextSplitter, PDFLoader
5
+ from aimakerspace.openai_utils.embedding import EmbeddingModel
6
+ from aimakerspace.vectordatabase import VectorDatabase
7
+ import asyncio
8
+
9
+ async def preprocess_files():
10
+ # Get all PDF files from the data directory
11
+ data_dir = "data"
12
+ pdf_files = [os.path.join(data_dir, f) for f in os.listdir(data_dir)
13
+ if f.lower().endswith('.pdf')]
14
+
15
+ if not pdf_files:
16
+ print("No PDF files found in the data directory!")
17
+ return
18
+
19
+ print(f"Found {len(pdf_files)} PDF files to process")
20
+
21
+ text_splitter = CharacterTextSplitter()
22
+ all_texts = []
23
+ all_metadata = []
24
+
25
+ # Load and process all PDF documents
26
+ for file_path in pdf_files:
27
+ print(f"Processing {file_path}")
28
+ loader = PDFLoader(file_path)
29
+ documents = loader.load_documents()
30
+
31
+ # Get pages from each document and extract text chunks
32
+ for doc_idx, doc in enumerate(documents):
33
+ # Extract page number if available
34
+ try:
35
+ page_num = doc_idx + 1 # Use document index as page number
36
+ texts = text_splitter.split_texts([doc])
37
+
38
+ for text in texts:
39
+ all_texts.append(text)
40
+ # Store metadata with each chunk
41
+ all_metadata.append({
42
+ "filename": os.path.basename(file_path),
43
+ "page": page_num
44
+ })
45
+ except Exception as e:
46
+ print(f"Error processing document: {e}")
47
+
48
+ print(f"Extracted {len(all_texts)} text chunks from all PDFs")
49
+
50
+ # Create vector database with embeddings
51
+ vector_db = VectorDatabase()
52
+ vector_db = await vector_db.abuild_from_list(all_texts)
53
+
54
+ # Save the processed data with metadata
55
+ with open('data/preprocessed_data.pkl', 'wb') as f:
56
+ pickle.dump({
57
+ 'texts': all_texts,
58
+ 'vectors': dict(vector_db.vectors),
59
+ 'metadata': all_metadata
60
+ }, f)
61
+
62
+ print("Preprocessing complete. Data saved to data/preprocessed_data.pkl")
63
+
64
+ if __name__ == "__main__":
65
+ asyncio.run(preprocess_files())
pyproject.toml ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ "numpy==2.2.2",
10
+ "openai==1.59.9",
11
+ "pydantic==2.10.1",
12
+ "pypdf2==3.0.1",
13
+ "websockets==14.2",
14
+ ]