Spaces:
Sleeping
Sleeping
Develop the streamlit app
Browse files- .dockerignore +4 -1
- .gitignore +6 -2
- Dockerfile +33 -9
- README.md +56 -12
- app.log +1 -0
- app.py +87 -126
- app_debug.py +302 -0
- debug_app.py +53 -0
- debug_output.log +1 -0
- debug_start.py +88 -0
- docker-compose.yml +1 -0
- huggingface-space.yml +14 -3
- minimal_app.py +19 -0
- notebook_version/.DS_Store +0 -0
- requirements.txt +3 -1
- run_log.txt +1 -0
- scripts/docker-entrypoint.sh +34 -0
- scripts/prepare_for_deployment.py +161 -0
- scripts/preprocess_data.py +247 -0
- streamlit_app.py +476 -0
.dockerignore
CHANGED
|
@@ -43,4 +43,7 @@ ENV/
|
|
| 43 |
notebook_version/
|
| 44 |
|
| 45 |
# Ignore PDF data files (will be mounted at runtime)
|
| 46 |
-
data/*.pdf
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
notebook_version/
|
| 44 |
|
| 45 |
# Ignore PDF data files (will be mounted at runtime)
|
| 46 |
+
data/*.pdf
|
| 47 |
+
|
| 48 |
+
# Pre-processed data (will be generated inside the container)
|
| 49 |
+
processed_data/
|
.gitignore
CHANGED
|
@@ -44,5 +44,9 @@ env/
|
|
| 44 |
# Mac OS
|
| 45 |
.DS_Store
|
| 46 |
|
| 47 |
-
# Local data
|
| 48 |
-
/data/*.pdf
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
# Mac OS
|
| 45 |
.DS_Store
|
| 46 |
|
| 47 |
+
# Local data - keep PDFs private and never commit them
|
| 48 |
+
/data/*.pdf
|
| 49 |
+
/data/**/*.pdf
|
| 50 |
+
|
| 51 |
+
# Do NOT ignore processed_data anymore - we want to commit this
|
| 52 |
+
# /processed_data/
|
Dockerfile
CHANGED
|
@@ -14,18 +14,42 @@ COPY requirements.txt .
|
|
| 14 |
# Install Python dependencies
|
| 15 |
RUN pip install --no-cache-dir -r requirements.txt
|
| 16 |
|
| 17 |
-
# Copy application code
|
| 18 |
COPY . .
|
| 19 |
|
| 20 |
-
# Create data directory for PDFs
|
| 21 |
RUN mkdir -p data
|
| 22 |
|
| 23 |
-
#
|
| 24 |
-
|
| 25 |
|
| 26 |
-
#
|
| 27 |
-
|
| 28 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
|
| 30 |
-
#
|
| 31 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
# Install Python dependencies
|
| 15 |
RUN pip install --no-cache-dir -r requirements.txt
|
| 16 |
|
| 17 |
+
# Copy the rest of the application code
|
| 18 |
COPY . .
|
| 19 |
|
| 20 |
+
# Create data directory for PDFs (if not already created)
|
| 21 |
RUN mkdir -p data
|
| 22 |
|
| 23 |
+
# Create directory for pre-processed data
|
| 24 |
+
RUN mkdir -p processed_data
|
| 25 |
|
| 26 |
+
# Configure Streamlit to run in headless mode (no welcome screen)
|
| 27 |
+
RUN mkdir -p /root/.streamlit && \
|
| 28 |
+
echo '[general]' > /root/.streamlit/config.toml && \
|
| 29 |
+
echo 'email = ""' >> /root/.streamlit/config.toml && \
|
| 30 |
+
echo 'showWarningOnDirectExecution = false' >> /root/.streamlit/config.toml && \
|
| 31 |
+
echo '' >> /root/.streamlit/config.toml && \
|
| 32 |
+
echo '[server]' >> /root/.streamlit/config.toml && \
|
| 33 |
+
echo 'headless = true' >> /root/.streamlit/config.toml
|
| 34 |
|
| 35 |
+
# Set environment variables
|
| 36 |
+
ENV HOST=0.0.0.0
|
| 37 |
+
ENV PORT=8000
|
| 38 |
+
|
| 39 |
+
# Allow both Chainlit and Streamlit apps to run (use environment variable to choose)
|
| 40 |
+
ENV APP_MODE=streamlit
|
| 41 |
+
|
| 42 |
+
# Create the entrypoint script
|
| 43 |
+
RUN echo '#!/bin/bash' > /app/entrypoint.sh && \
|
| 44 |
+
echo 'if [ "$APP_MODE" = "chainlit" ]; then' >> /app/entrypoint.sh && \
|
| 45 |
+
echo ' chainlit run app.py --host $HOST --port $PORT' >> /app/entrypoint.sh && \
|
| 46 |
+
echo 'else' >> /app/entrypoint.sh && \
|
| 47 |
+
echo ' streamlit run streamlit_app.py --server.address $HOST --server.port $PORT' >> /app/entrypoint.sh && \
|
| 48 |
+
echo 'fi' >> /app/entrypoint.sh && \
|
| 49 |
+
chmod +x /app/entrypoint.sh
|
| 50 |
+
|
| 51 |
+
# Expose the port
|
| 52 |
+
EXPOSE $PORT
|
| 53 |
+
|
| 54 |
+
# Run the application
|
| 55 |
+
ENTRYPOINT ["/app/entrypoint.sh"]
|
README.md
CHANGED
|
@@ -8,6 +8,8 @@ An application that helps answer questions about AB Testing using a collection o
|
|
| 8 |
- **Query Rephrasing**: Improves retrieval by rephrasing your query for better results
|
| 9 |
- **Source References**: Shows the exact document sources used to answer your question
|
| 10 |
- **Streaming Interface**: See the response as it's being generated
|
|
|
|
|
|
|
| 11 |
|
| 12 |
## Prerequisites
|
| 13 |
|
|
@@ -52,12 +54,17 @@ An application that helps answer questions about AB Testing using a collection o
|
|
| 52 |
# Copy your AB Testing PDFs to the data directory
|
| 53 |
```
|
| 54 |
|
| 55 |
-
7.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
```bash
|
| 57 |
chainlit run app.py
|
| 58 |
```
|
| 59 |
|
| 60 |
-
|
| 61 |
|
| 62 |
### Using Docker
|
| 63 |
|
|
@@ -83,28 +90,65 @@ An application that helps answer questions about AB Testing using a collection o
|
|
| 83 |
# Copy your AB Testing PDFs to the data directory
|
| 84 |
```
|
| 85 |
|
| 86 |
-
5. Build and run with Docker Compose
|
| 87 |
```bash
|
| 88 |
docker-compose up -d
|
| 89 |
```
|
| 90 |
|
| 91 |
6. Open your browser to `http://localhost:8000`
|
| 92 |
|
| 93 |
-
## Deploying to Hugging Face Spaces
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
|
| 95 |
-
|
| 96 |
-
2. Set the following environment variables in your Space settings:
|
| 97 |
- `OPENAI_API_KEY`: Your OpenAI API key
|
| 98 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
|
| 100 |
## How It Works
|
| 101 |
|
| 102 |
-
1. The application loads
|
| 103 |
-
2.
|
| 104 |
-
3. Qdrant vector store is used for semantic search
|
| 105 |
-
4. When a user asks a question:
|
| 106 |
- The query is rephrased to be more specific for better retrieval
|
| 107 |
-
- Relevant document chunks are retrieved
|
| 108 |
- OpenAI's GPT model generates an answer based on the retrieved context
|
| 109 |
- Source references are tracked and displayed alongside the answer
|
| 110 |
|
|
|
|
| 8 |
- **Query Rephrasing**: Improves retrieval by rephrasing your query for better results
|
| 9 |
- **Source References**: Shows the exact document sources used to answer your question
|
| 10 |
- **Streaming Interface**: See the response as it's being generated
|
| 11 |
+
- **Pre-processed Data**: Faster startup with pre-processed document embeddings
|
| 12 |
+
- **Privacy Preserving**: Deploy with pre-processed data only, keeping source PDFs private
|
| 13 |
|
| 14 |
## Prerequisites
|
| 15 |
|
|
|
|
| 54 |
# Copy your AB Testing PDFs to the data directory
|
| 55 |
```
|
| 56 |
|
| 57 |
+
7. Pre-process the PDF files (this step significantly speeds up application startup)
|
| 58 |
+
```bash
|
| 59 |
+
python scripts/preprocess_data.py
|
| 60 |
+
```
|
| 61 |
+
|
| 62 |
+
8. Run the application
|
| 63 |
```bash
|
| 64 |
chainlit run app.py
|
| 65 |
```
|
| 66 |
|
| 67 |
+
9. Open your browser to `http://localhost:8000`
|
| 68 |
|
| 69 |
### Using Docker
|
| 70 |
|
|
|
|
| 90 |
# Copy your AB Testing PDFs to the data directory
|
| 91 |
```
|
| 92 |
|
| 93 |
+
5. Build and run with Docker Compose (the container will automatically pre-process PDFs at startup)
|
| 94 |
```bash
|
| 95 |
docker-compose up -d
|
| 96 |
```
|
| 97 |
|
| 98 |
6. Open your browser to `http://localhost:8000`
|
| 99 |
|
| 100 |
+
## Deploying to Hugging Face Spaces (Privacy-Preserving Method)
|
| 101 |
+
|
| 102 |
+
This method allows you to deploy your app without exposing your source PDF files:
|
| 103 |
+
|
| 104 |
+
1. Pre-process your PDFs locally:
|
| 105 |
+
```bash
|
| 106 |
+
# Ensure PDFs are in the data directory
|
| 107 |
+
python scripts/preprocess_data.py
|
| 108 |
+
```
|
| 109 |
+
|
| 110 |
+
2. Commit only the pre-processed data (not the PDFs) to your repository:
|
| 111 |
+
```bash
|
| 112 |
+
# PDFs are excluded in .gitignore already
|
| 113 |
+
git add processed_data app.py requirements.txt Dockerfile scripts/docker-entrypoint.sh
|
| 114 |
+
git commit -m "Add pre-processed data for deployment"
|
| 115 |
+
```
|
| 116 |
+
|
| 117 |
+
3. Create a new Hugging Face Space with Docker deployment
|
| 118 |
|
| 119 |
+
4. Set the following environment variables in your Space settings:
|
|
|
|
| 120 |
- `OPENAI_API_KEY`: Your OpenAI API key
|
| 121 |
+
|
| 122 |
+
5. Push your code to the Space's repository:
|
| 123 |
+
```bash
|
| 124 |
+
git push huggingface main
|
| 125 |
+
```
|
| 126 |
+
|
| 127 |
+
The advantage of this approach is that:
|
| 128 |
+
- Your original PDF files remain private and are never uploaded
|
| 129 |
+
- Only the pre-processed embeddings and chunks are deployed
|
| 130 |
+
- The app will work immediately without needing to process PDFs at runtime
|
| 131 |
+
|
| 132 |
+
## Pre-processing Explained
|
| 133 |
+
|
| 134 |
+
The pre-processing step:
|
| 135 |
+
1. Loads PDF files from the `data` directory
|
| 136 |
+
2. Splits documents into chunks with correct page references
|
| 137 |
+
3. Generates embeddings for each chunk using OpenAI's embedding model
|
| 138 |
+
4. Saves both document chunks and the vector database to disk
|
| 139 |
+
|
| 140 |
+
This approach has several advantages:
|
| 141 |
+
- The app starts up much faster since it doesn't need to process PDFs at runtime
|
| 142 |
+
- You don't need to upload large PDF files to Hugging Face
|
| 143 |
+
- The vector database is persisted, providing consistent performance
|
| 144 |
+
- You can deploy without exposing your source materials
|
| 145 |
|
| 146 |
## How It Works
|
| 147 |
|
| 148 |
+
1. The application loads pre-processed document chunks and vector store from disk
|
| 149 |
+
2. When a user asks a question:
|
|
|
|
|
|
|
| 150 |
- The query is rephrased to be more specific for better retrieval
|
| 151 |
+
- Relevant document chunks are retrieved from the vector store
|
| 152 |
- OpenAI's GPT model generates an answer based on the retrieved context
|
| 153 |
- Source references are tracked and displayed alongside the answer
|
| 154 |
|
app.log
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
2025-04-29 21:20:25 - Loaded .env file
|
app.py
CHANGED
|
@@ -1,149 +1,106 @@
|
|
| 1 |
import os
|
|
|
|
| 2 |
import chainlit as cl
|
| 3 |
-
from
|
| 4 |
from chainlit.element import Text
|
| 5 |
from langchain_core.prompts import ChatPromptTemplate
|
| 6 |
from langchain.schema.output_parser import StrOutputParser
|
| 7 |
-
from langchain_community.document_loaders import DirectoryLoader
|
| 8 |
-
from langchain_community.document_loaders import PyPDFLoader
|
| 9 |
from langchain_openai.embeddings import OpenAIEmbeddings
|
| 10 |
-
from
|
| 11 |
-
from langchain.text_splitter import RecursiveCharacterTextSplitter
|
| 12 |
-
from langchain_core.runnables import RunnablePassthrough
|
| 13 |
-
from langchain_core.messages import AIMessage, HumanMessage
|
| 14 |
-
from operator import itemgetter
|
| 15 |
-
from collections import defaultdict
|
| 16 |
-
from langchain_core.documents import Document
|
| 17 |
-
import tiktoken
|
| 18 |
-
import re
|
| 19 |
import functools
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
# Configure OpenAI API key from environment variable
|
| 22 |
if not os.environ.get("OPENAI_API_KEY"):
|
| 23 |
os.environ["OPENAI_API_KEY"] = os.environ.get("OPENAI_API_KEY_BACKUP", "")
|
| 24 |
|
| 25 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
@cl.cache
|
| 27 |
-
def
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
|
| 33 |
-
#
|
| 34 |
-
|
| 35 |
-
current_index = 0
|
| 36 |
|
| 37 |
-
#
|
| 38 |
-
|
| 39 |
|
| 40 |
-
#
|
| 41 |
-
|
| 42 |
-
source = doc.metadata.get("source", "")
|
| 43 |
-
docs_by_source[source].append(doc)
|
| 44 |
|
| 45 |
-
#
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
source_docs.sort(key=lambda x: x.metadata.get("page", 0))
|
| 50 |
-
|
| 51 |
-
# Merge the content
|
| 52 |
-
merged_content = ""
|
| 53 |
-
page_ranges = []
|
| 54 |
-
current_pos = 0
|
| 55 |
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
merged_content += "\n\n"
|
| 63 |
-
|
| 64 |
-
# Record where this page's content starts in the merged document
|
| 65 |
-
start_pos = len(merged_content)
|
| 66 |
-
merged_content += doc.page_content
|
| 67 |
-
end_pos = len(merged_content)
|
| 68 |
-
|
| 69 |
-
# Store the mapping of character ranges to original page numbers
|
| 70 |
-
page_ranges.append({
|
| 71 |
-
"start": start_pos,
|
| 72 |
-
"end": end_pos,
|
| 73 |
-
"page": page_num,
|
| 74 |
-
"source": source
|
| 75 |
-
})
|
| 76 |
|
| 77 |
-
#
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
"page_ranges": page_ranges # Store the page ranges for later reference
|
| 84 |
-
}
|
| 85 |
|
| 86 |
-
|
| 87 |
-
merged_doc = Document(page_content=merged_content, metadata=merged_metadata)
|
| 88 |
-
merged_docs.append(merged_doc)
|
| 89 |
-
|
| 90 |
-
# tiktoken_len counts tokens (not characters) using the gpt-4o-mini tokenizer
|
| 91 |
-
def tiktoken_len(text):
|
| 92 |
-
tokens = tiktoken.encoding_for_model("gpt-4o-mini").encode(
|
| 93 |
-
text,
|
| 94 |
-
)
|
| 95 |
-
return len(tokens)
|
| 96 |
|
| 97 |
-
#
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
# Find which page this chunk belongs to
|
| 105 |
-
if "page_ranges" in split.metadata:
|
| 106 |
-
for page_range in split.metadata["page_ranges"]:
|
| 107 |
-
# If chunk significantly overlaps with this page range
|
| 108 |
-
if (start_pos <= page_range["end"] and
|
| 109 |
-
end_pos >= page_range["start"]):
|
| 110 |
-
# Use this page number
|
| 111 |
-
split.metadata["page"] = page_range["page"]
|
| 112 |
-
break
|
| 113 |
-
return splits
|
| 114 |
-
|
| 115 |
-
# Split the text with start index tracking
|
| 116 |
-
text_splitter = RecursiveCharacterTextSplitter(
|
| 117 |
-
chunk_size=300,
|
| 118 |
-
chunk_overlap=50,
|
| 119 |
-
length_function=tiktoken_len,
|
| 120 |
-
add_start_index=True
|
| 121 |
)
|
| 122 |
|
| 123 |
-
|
| 124 |
-
raw_splits = text_splitter.split_documents(merged_docs)
|
| 125 |
-
split_chunks = add_page_info_to_splits(raw_splits)
|
| 126 |
-
|
| 127 |
-
return split_chunks
|
| 128 |
|
| 129 |
-
#
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
#
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
)
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
# Setup vector store at startup
|
| 145 |
-
vectorstore = setup_vector_store()
|
| 146 |
-
retriever = vectorstore.as_retriever(search_kwargs={"k": 5})
|
| 147 |
|
| 148 |
# Define prompts
|
| 149 |
RAG_PROMPT = """
|
|
@@ -260,13 +217,14 @@ async def on_message(message: cl.Message):
|
|
| 260 |
|
| 261 |
# Stream the response
|
| 262 |
final_answer = await chat_model.astream(
|
| 263 |
-
[
|
| 264 |
-
|
| 265 |
-
|
|
|
|
| 266 |
context=retrieval_result.get("context", ""),
|
| 267 |
question=query
|
| 268 |
)
|
| 269 |
-
|
| 270 |
],
|
| 271 |
callbacks=[
|
| 272 |
cl.AsyncLangchainCallbackHandler(
|
|
@@ -281,6 +239,9 @@ async def on_message(message: cl.Message):
|
|
| 281 |
source_elements = []
|
| 282 |
for i, source in enumerate(sources):
|
| 283 |
title = source.get("title", "Unknown")
|
|
|
|
|
|
|
|
|
|
| 284 |
page = source.get("page", "Unknown")
|
| 285 |
|
| 286 |
source_elements.append(
|
|
|
|
| 1 |
import os
|
| 2 |
+
import pickle
|
| 3 |
import chainlit as cl
|
| 4 |
+
from langchain_openai.chat_models import ChatOpenAI
|
| 5 |
from chainlit.element import Text
|
| 6 |
from langchain_core.prompts import ChatPromptTemplate
|
| 7 |
from langchain.schema.output_parser import StrOutputParser
|
|
|
|
|
|
|
| 8 |
from langchain_openai.embeddings import OpenAIEmbeddings
|
| 9 |
+
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
import functools
|
| 11 |
+
from qdrant_client import QdrantClient
|
| 12 |
+
from langchain_core.vectorstores import VectorStoreRetriever
|
| 13 |
+
from langchain_core.documents import Document
|
| 14 |
|
| 15 |
# Configure OpenAI API key from environment variable
|
| 16 |
if not os.environ.get("OPENAI_API_KEY"):
|
| 17 |
os.environ["OPENAI_API_KEY"] = os.environ.get("OPENAI_API_KEY_BACKUP", "")
|
| 18 |
|
| 19 |
+
# Paths to pre-processed data
|
| 20 |
+
PROCESSED_DATA_DIR = Path("processed_data")
|
| 21 |
+
CHUNKS_FILE = PROCESSED_DATA_DIR / "document_chunks.pkl"
|
| 22 |
+
QDRANT_DIR = PROCESSED_DATA_DIR / "qdrant_vectorstore"
|
| 23 |
+
|
| 24 |
+
# Check if pre-processed data exists
|
| 25 |
+
def check_preprocessed_data():
|
| 26 |
+
"""Check if pre-processed data exists and is readable."""
|
| 27 |
+
if not PROCESSED_DATA_DIR.exists():
|
| 28 |
+
raise FileNotFoundError(f"Processed data directory not found: {PROCESSED_DATA_DIR}. Please run scripts/preprocess_data.py first.")
|
| 29 |
+
|
| 30 |
+
if not CHUNKS_FILE.exists():
|
| 31 |
+
raise FileNotFoundError(f"Document chunks file not found: {CHUNKS_FILE}. Please run scripts/preprocess_data.py first.")
|
| 32 |
+
|
| 33 |
+
if not QDRANT_DIR.exists():
|
| 34 |
+
raise FileNotFoundError(f"Vector store directory not found: {QDRANT_DIR}. Please run scripts/preprocess_data.py first.")
|
| 35 |
+
|
| 36 |
+
# Load pre-processed document chunks
|
| 37 |
@cl.cache
|
| 38 |
+
def load_document_chunks():
|
| 39 |
+
"""Load pre-processed document chunks from disk."""
|
| 40 |
+
with open(CHUNKS_FILE, 'rb') as f:
|
| 41 |
+
return pickle.load(f)
|
| 42 |
+
|
| 43 |
+
# Load and create a custom retriever for Qdrant
|
| 44 |
+
@cl.cache
|
| 45 |
+
def load_qdrant_retriever():
|
| 46 |
+
"""Load Qdrant client and create a retriever."""
|
| 47 |
+
embedding_model = OpenAIEmbeddings(model="text-embedding-3-small")
|
| 48 |
|
| 49 |
+
# Load the document chunks for metadata access
|
| 50 |
+
chunks = load_document_chunks()
|
|
|
|
| 51 |
|
| 52 |
+
# Create a dictionary mapping IDs to documents for quick lookup
|
| 53 |
+
docs_by_id = {i: doc for i, doc in enumerate(chunks)}
|
| 54 |
|
| 55 |
+
# Initialize Qdrant client
|
| 56 |
+
client = QdrantClient(path=str(QDRANT_DIR))
|
|
|
|
|
|
|
| 57 |
|
| 58 |
+
# Create a custom retriever function
|
| 59 |
+
def retrieve_docs(query, k=5):
|
| 60 |
+
# Get query embedding
|
| 61 |
+
query_embedding = embedding_model.embed_query(query)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
|
| 63 |
+
# Search Qdrant
|
| 64 |
+
results = client.search(
|
| 65 |
+
collection_name="kohavi_ab_testing_pdf_collection",
|
| 66 |
+
query_vector=query_embedding,
|
| 67 |
+
limit=k
|
| 68 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
|
| 70 |
+
# Convert results to documents
|
| 71 |
+
documents = []
|
| 72 |
+
for result in results:
|
| 73 |
+
doc_id = result.id
|
| 74 |
+
if doc_id in docs_by_id:
|
| 75 |
+
documents.append(docs_by_id[doc_id])
|
|
|
|
|
|
|
| 76 |
|
| 77 |
+
return documents
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
|
| 79 |
+
# Create a VectorStoreRetriever from the custom retriever function
|
| 80 |
+
retriever = VectorStoreRetriever(
|
| 81 |
+
vectorstore=None, # Not using a standard vectorstore
|
| 82 |
+
search_type="similarity",
|
| 83 |
+
search_kwargs={"k": 5},
|
| 84 |
+
retriever_fn=retrieve_docs
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
)
|
| 86 |
|
| 87 |
+
return retriever
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
|
| 89 |
+
# Check for pre-processed data at startup
|
| 90 |
+
try:
|
| 91 |
+
check_preprocessed_data()
|
| 92 |
+
# Load document chunks
|
| 93 |
+
docs = load_document_chunks()
|
| 94 |
+
# Load retriever
|
| 95 |
+
retriever = load_qdrant_retriever()
|
| 96 |
+
print(f"Successfully loaded pre-processed data with {len(docs)} document chunks")
|
| 97 |
+
except FileNotFoundError as e:
|
| 98 |
+
# If pre-processed data doesn't exist, print a helpful error message
|
| 99 |
+
print(f"ERROR: {e}")
|
| 100 |
+
print("\nTo pre-process your data, run:")
|
| 101 |
+
print(" python scripts/preprocess_data.py")
|
| 102 |
+
# Raise the error to prevent the app from starting without pre-processed data
|
| 103 |
+
raise
|
|
|
|
|
|
|
|
|
|
| 104 |
|
| 105 |
# Define prompts
|
| 106 |
RAG_PROMPT = """
|
|
|
|
| 217 |
|
| 218 |
# Stream the response
|
| 219 |
final_answer = await chat_model.astream(
|
| 220 |
+
messages=[
|
| 221 |
+
{
|
| 222 |
+
"role": "user",
|
| 223 |
+
"content": rag_prompt.format(
|
| 224 |
context=retrieval_result.get("context", ""),
|
| 225 |
question=query
|
| 226 |
)
|
| 227 |
+
}
|
| 228 |
],
|
| 229 |
callbacks=[
|
| 230 |
cl.AsyncLangchainCallbackHandler(
|
|
|
|
| 239 |
source_elements = []
|
| 240 |
for i, source in enumerate(sources):
|
| 241 |
title = source.get("title", "Unknown")
|
| 242 |
+
# Remove the .pdf extension if present
|
| 243 |
+
if title.lower().endswith('.pdf'):
|
| 244 |
+
title = title[:-4] # Remove the last 4 characters (.pdf)
|
| 245 |
page = source.get("page", "Unknown")
|
| 246 |
|
| 247 |
source_elements.append(
|
app_debug.py
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import pickle
|
| 3 |
+
import sys
|
| 4 |
+
import chainlit as cl
|
| 5 |
+
from langchain_openai.chat_models import ChatOpenAI
|
| 6 |
+
from chainlit.element import Text
|
| 7 |
+
from langchain_core.prompts import ChatPromptTemplate
|
| 8 |
+
from langchain.schema.output_parser import StrOutputParser
|
| 9 |
+
from langchain_openai.embeddings import OpenAIEmbeddings
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
import functools
|
| 12 |
+
from qdrant_client import QdrantClient
|
| 13 |
+
from langchain_core.vectorstores import VectorStoreRetriever
|
| 14 |
+
from langchain_core.documents import Document
|
| 15 |
+
|
| 16 |
+
print("Starting app_debug.py execution...")
|
| 17 |
+
|
| 18 |
+
# Configure OpenAI API key from environment variable
|
| 19 |
+
if not os.environ.get("OPENAI_API_KEY"):
|
| 20 |
+
os.environ["OPENAI_API_KEY"] = os.environ.get("OPENAI_API_KEY_BACKUP", "")
|
| 21 |
+
print("Configured OpenAI API key")
|
| 22 |
+
|
| 23 |
+
# Paths to pre-processed data
|
| 24 |
+
PROCESSED_DATA_DIR = Path("processed_data")
|
| 25 |
+
CHUNKS_FILE = PROCESSED_DATA_DIR / "document_chunks.pkl"
|
| 26 |
+
QDRANT_DIR = PROCESSED_DATA_DIR / "qdrant_vectorstore"
|
| 27 |
+
print(f"Configured paths: {PROCESSED_DATA_DIR}, {CHUNKS_FILE}, {QDRANT_DIR}")
|
| 28 |
+
|
| 29 |
+
# Check if pre-processed data exists
|
| 30 |
+
def check_preprocessed_data():
|
| 31 |
+
"""Check if pre-processed data exists and is readable."""
|
| 32 |
+
print("Checking preprocessed data...")
|
| 33 |
+
if not PROCESSED_DATA_DIR.exists():
|
| 34 |
+
raise FileNotFoundError(f"Processed data directory not found: {PROCESSED_DATA_DIR}. Please run scripts/preprocess_data.py first.")
|
| 35 |
+
|
| 36 |
+
if not CHUNKS_FILE.exists():
|
| 37 |
+
raise FileNotFoundError(f"Document chunks file not found: {CHUNKS_FILE}. Please run scripts/preprocess_data.py first.")
|
| 38 |
+
|
| 39 |
+
if not QDRANT_DIR.exists():
|
| 40 |
+
raise FileNotFoundError(f"Vector store directory not found: {QDRANT_DIR}. Please run scripts/preprocess_data.py first.")
|
| 41 |
+
print("Preprocessed data checks passed")
|
| 42 |
+
|
| 43 |
+
# Load pre-processed document chunks
|
| 44 |
+
@cl.cache
|
| 45 |
+
def load_document_chunks():
|
| 46 |
+
"""Load pre-processed document chunks from disk."""
|
| 47 |
+
print("Loading document chunks...")
|
| 48 |
+
with open(CHUNKS_FILE, 'rb') as f:
|
| 49 |
+
chunks = pickle.load(f)
|
| 50 |
+
print(f"Loaded {len(chunks)} document chunks")
|
| 51 |
+
return chunks
|
| 52 |
+
|
| 53 |
+
# Load and create a custom retriever for Qdrant
|
| 54 |
+
@cl.cache
|
| 55 |
+
def load_qdrant_retriever():
|
| 56 |
+
"""Load Qdrant client and create a retriever."""
|
| 57 |
+
print("Creating Qdrant retriever...")
|
| 58 |
+
embedding_model = OpenAIEmbeddings(model="text-embedding-3-small")
|
| 59 |
+
print("Created embedding model")
|
| 60 |
+
|
| 61 |
+
# Load the document chunks for metadata access
|
| 62 |
+
chunks = load_document_chunks()
|
| 63 |
+
|
| 64 |
+
# Create a dictionary mapping IDs to documents for quick lookup
|
| 65 |
+
docs_by_id = {i: doc for i, doc in enumerate(chunks)}
|
| 66 |
+
print(f"Created mapping of {len(docs_by_id)} documents")
|
| 67 |
+
|
| 68 |
+
# Initialize Qdrant client
|
| 69 |
+
print(f"Initializing Qdrant client at {QDRANT_DIR}...")
|
| 70 |
+
client = QdrantClient(path=str(QDRANT_DIR))
|
| 71 |
+
print("Initialized Qdrant client")
|
| 72 |
+
|
| 73 |
+
# Create a custom retriever function
|
| 74 |
+
def retrieve_docs(query, k=5):
|
| 75 |
+
print(f"Retrieving documents for query: {query[:30]}...")
|
| 76 |
+
# Get query embedding
|
| 77 |
+
query_embedding = embedding_model.embed_query(query)
|
| 78 |
+
|
| 79 |
+
# Search Qdrant
|
| 80 |
+
results = client.search(
|
| 81 |
+
collection_name="kohavi_ab_testing_pdf_collection",
|
| 82 |
+
query_vector=query_embedding,
|
| 83 |
+
limit=k
|
| 84 |
+
)
|
| 85 |
+
|
| 86 |
+
# Convert results to documents
|
| 87 |
+
documents = []
|
| 88 |
+
for result in results:
|
| 89 |
+
doc_id = result.id
|
| 90 |
+
if doc_id in docs_by_id:
|
| 91 |
+
documents.append(docs_by_id[doc_id])
|
| 92 |
+
print(f"Retrieved {len(documents)} documents")
|
| 93 |
+
return documents
|
| 94 |
+
|
| 95 |
+
# Create a VectorStoreRetriever from the custom retriever function
|
| 96 |
+
retriever = VectorStoreRetriever(
|
| 97 |
+
vectorstore=None, # Not using a standard vectorstore
|
| 98 |
+
search_type="similarity",
|
| 99 |
+
search_kwargs={"k": 5},
|
| 100 |
+
retriever_fn=retrieve_docs
|
| 101 |
+
)
|
| 102 |
+
print("Created retriever")
|
| 103 |
+
return retriever
|
| 104 |
+
|
| 105 |
+
# Check for pre-processed data at startup
|
| 106 |
+
try:
|
| 107 |
+
print("Checking for pre-processed data...")
|
| 108 |
+
check_preprocessed_data()
|
| 109 |
+
# Load document chunks
|
| 110 |
+
print("Loading document chunks...")
|
| 111 |
+
docs = load_document_chunks()
|
| 112 |
+
# Load retriever
|
| 113 |
+
print("Loading retriever...")
|
| 114 |
+
retriever = load_qdrant_retriever()
|
| 115 |
+
print(f"Successfully loaded pre-processed data with {len(docs)} document chunks")
|
| 116 |
+
except FileNotFoundError as e:
|
| 117 |
+
# If pre-processed data doesn't exist, print a helpful error message
|
| 118 |
+
print(f"ERROR: {e}")
|
| 119 |
+
print("\nTo pre-process your data, run:")
|
| 120 |
+
print(" python scripts/preprocess_data.py")
|
| 121 |
+
# Raise the error to prevent the app from starting without pre-processed data
|
| 122 |
+
raise
|
| 123 |
+
|
| 124 |
+
# Define prompts
|
| 125 |
+
RAG_PROMPT = """
|
| 126 |
+
CONTEXT:
|
| 127 |
+
{context}
|
| 128 |
+
|
| 129 |
+
QUERY:
|
| 130 |
+
{question}
|
| 131 |
+
|
| 132 |
+
You are a helpful assistant. Use the available context to answer the question. Do not use your own knowledge! If you cannot answer the question based on the context, you must say "I don't know".
|
| 133 |
+
"""
|
| 134 |
+
|
| 135 |
+
REPHRASE_QUERY_PROMPT = """
|
| 136 |
+
QUERY:
|
| 137 |
+
{question}
|
| 138 |
+
|
| 139 |
+
You are a helpful assistant. Rephrase the provided query to be more specific and to the point in order to improve retrieval in our RAG pipeline about AB Testing.
|
| 140 |
+
"""
|
| 141 |
+
|
| 142 |
+
rag_prompt = ChatPromptTemplate.from_template(RAG_PROMPT)
|
| 143 |
+
rephrase_query_prompt = ChatPromptTemplate.from_template(REPHRASE_QUERY_PROMPT)
|
| 144 |
+
print("Defined prompts")
|
| 145 |
+
|
| 146 |
+
# Setup chat model
|
| 147 |
+
def get_openai_chat_model():
|
| 148 |
+
print("Creating OpenAI chat model...")
|
| 149 |
+
model = ChatOpenAI(
|
| 150 |
+
model="gpt-4-turbo",
|
| 151 |
+
temperature=0,
|
| 152 |
+
streaming=True,
|
| 153 |
+
)
|
| 154 |
+
print("Created OpenAI chat model")
|
| 155 |
+
return model
|
| 156 |
+
|
| 157 |
+
# Setup rephrased retrieval chain
|
| 158 |
+
def setup_rephrased_retriever_chain():
|
| 159 |
+
print("Setting up rephrased retriever chain...")
|
| 160 |
+
chat_model = get_openai_chat_model()
|
| 161 |
+
|
| 162 |
+
def retrieve_and_format_documents(query):
|
| 163 |
+
print(f"Retrieving documents for: {query[:30]}...")
|
| 164 |
+
rephrased_query_chain = rephrase_query_prompt | chat_model | StrOutputParser()
|
| 165 |
+
|
| 166 |
+
@cl.step(name="Rephrasing query for better retrieval")
|
| 167 |
+
async def rephrase_query():
|
| 168 |
+
print("Rephrasing query...")
|
| 169 |
+
rephrased_query = await cl.make_async(rephrased_query_chain.invoke)({"question": query})
|
| 170 |
+
print(f"Rephrased query: {rephrased_query[:30]}...")
|
| 171 |
+
await cl.Message(content=f"Rephrased query: {rephrased_query}").send()
|
| 172 |
+
return rephrased_query
|
| 173 |
+
|
| 174 |
+
rephrased_query_result = cl.run_sync(rephrase_query())
|
| 175 |
+
print(f"Received rephrased query: {rephrased_query_result[:30]}...")
|
| 176 |
+
|
| 177 |
+
# Get relevant documents based on rephrased query
|
| 178 |
+
docs = retriever.get_relevant_documents(rephrased_query_result)
|
| 179 |
+
print(f"Retrieved {len(docs)} documents")
|
| 180 |
+
|
| 181 |
+
# Extract sources from the documents for later display
|
| 182 |
+
sources = []
|
| 183 |
+
for doc in docs:
|
| 184 |
+
source_path = doc.metadata.get("source", "")
|
| 185 |
+
filename = source_path.split("/")[-1] if "/" in source_path else source_path
|
| 186 |
+
|
| 187 |
+
sources.append({
|
| 188 |
+
"title": filename,
|
| 189 |
+
"page": doc.metadata.get("page", "unknown"),
|
| 190 |
+
})
|
| 191 |
+
|
| 192 |
+
# Format documents into a string for the context
|
| 193 |
+
formatted_docs = "\n\n".join([doc.page_content for doc in docs])
|
| 194 |
+
print(f"Formatted {len(docs)} documents")
|
| 195 |
+
|
| 196 |
+
return {"context": formatted_docs, "sources": sources, "question": query}
|
| 197 |
+
|
| 198 |
+
print("Created retrieve_and_format_documents function")
|
| 199 |
+
return retrieve_and_format_documents
|
| 200 |
+
|
| 201 |
+
print("Defined setup functions")
|
| 202 |
+
|
| 203 |
+
@cl.on_chat_start
|
| 204 |
+
async def on_chat_start():
|
| 205 |
+
print("on_chat_start called")
|
| 206 |
+
# Initialize the chat model and chain in the user session
|
| 207 |
+
chat_model = get_openai_chat_model()
|
| 208 |
+
retriever_chain = setup_rephrased_retriever_chain()
|
| 209 |
+
|
| 210 |
+
# Store in user session
|
| 211 |
+
cl.user_session.set("chat_model", chat_model)
|
| 212 |
+
cl.user_session.set("retriever_chain", retriever_chain)
|
| 213 |
+
print("Stored chat model and retriever chain in user session")
|
| 214 |
+
|
| 215 |
+
welcome_message = """
|
| 216 |
+
# ๐ AB Testing RAG Agent
|
| 217 |
+
|
| 218 |
+
This agent can answer questions about AB Testing using a collection of PDF documents.
|
| 219 |
+
|
| 220 |
+
**Examples of questions you can ask:**
|
| 221 |
+
- What is AB Testing?
|
| 222 |
+
- How do I interpret p-values in experiments?
|
| 223 |
+
- What are the best practices for running experiments with low traffic?
|
| 224 |
+
- How should I choose metrics for my experiments?
|
| 225 |
+
"""
|
| 226 |
+
|
| 227 |
+
await cl.Message(content=welcome_message).send()
|
| 228 |
+
print("Sent welcome message")
|
| 229 |
+
|
| 230 |
+
@cl.on_message
|
| 231 |
+
async def on_message(message: cl.Message):
|
| 232 |
+
print(f"on_message called with content: {message.content[:30]}...")
|
| 233 |
+
# Get the chat model and retriever chain from user session
|
| 234 |
+
chat_model = cl.user_session.get("chat_model")
|
| 235 |
+
retriever_chain = cl.user_session.get("retriever_chain")
|
| 236 |
+
print("Retrieved chat model and retriever chain from user session")
|
| 237 |
+
|
| 238 |
+
query = message.content
|
| 239 |
+
|
| 240 |
+
# Step 1: Retrieve documents and extract sources
|
| 241 |
+
@cl.step(name="Retrieving relevant documents")
|
| 242 |
+
async def retrieve_docs():
|
| 243 |
+
print("retrieve_docs step started")
|
| 244 |
+
result = await cl.make_async(retriever_chain)(query)
|
| 245 |
+
print("retrieve_docs step completed")
|
| 246 |
+
return result
|
| 247 |
+
|
| 248 |
+
retrieval_result = await retrieve_docs()
|
| 249 |
+
print("Retrieved documents and extracted sources")
|
| 250 |
+
|
| 251 |
+
# Store sources for later display
|
| 252 |
+
sources = retrieval_result.get("sources", [])
|
| 253 |
+
|
| 254 |
+
# Step 2: Generate response with the retrieved context
|
| 255 |
+
msg = cl.Message(content="")
|
| 256 |
+
await msg.send()
|
| 257 |
+
print("Sent empty message to be streamed to")
|
| 258 |
+
|
| 259 |
+
# Stream the response
|
| 260 |
+
final_answer = await chat_model.astream(
|
| 261 |
+
messages=[
|
| 262 |
+
{
|
| 263 |
+
"role": "user",
|
| 264 |
+
"content": rag_prompt.format(
|
| 265 |
+
context=retrieval_result.get("context", ""),
|
| 266 |
+
question=query
|
| 267 |
+
)
|
| 268 |
+
}
|
| 269 |
+
],
|
| 270 |
+
callbacks=[
|
| 271 |
+
cl.AsyncLangchainCallbackHandler(
|
| 272 |
+
stream_to_message=msg,
|
| 273 |
+
append_to_message=True,
|
| 274 |
+
)
|
| 275 |
+
],
|
| 276 |
+
)
|
| 277 |
+
print("Streamed response from OpenAI")
|
| 278 |
+
|
| 279 |
+
# Display sources
|
| 280 |
+
if sources:
|
| 281 |
+
source_elements = []
|
| 282 |
+
for i, source in enumerate(sources):
|
| 283 |
+
title = source.get("title", "Unknown")
|
| 284 |
+
# Remove the .pdf extension if present
|
| 285 |
+
if title.lower().endswith('.pdf'):
|
| 286 |
+
title = title[:-4] # Remove the last 4 characters (.pdf)
|
| 287 |
+
page = source.get("page", "Unknown")
|
| 288 |
+
|
| 289 |
+
source_elements.append(
|
| 290 |
+
Text(
|
| 291 |
+
name=f"Source {i+1}",
|
| 292 |
+
content=f"{title}, Page: {page}",
|
| 293 |
+
display="inline",
|
| 294 |
+
)
|
| 295 |
+
)
|
| 296 |
+
|
| 297 |
+
await msg.elements.extend(source_elements)
|
| 298 |
+
await msg.update()
|
| 299 |
+
print("Updated message with source elements")
|
| 300 |
+
|
| 301 |
+
print("Defined event handlers")
|
| 302 |
+
print("Ready to start Chainlit app")
|
debug_app.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import pickle
|
| 3 |
+
import sys
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from qdrant_client import QdrantClient
|
| 6 |
+
|
| 7 |
+
# Paths to pre-processed data
|
| 8 |
+
PROCESSED_DATA_DIR = Path("processed_data")
|
| 9 |
+
CHUNKS_FILE = PROCESSED_DATA_DIR / "document_chunks.pkl"
|
| 10 |
+
QDRANT_DIR = PROCESSED_DATA_DIR / "qdrant_vectorstore"
|
| 11 |
+
|
| 12 |
+
def main():
|
| 13 |
+
print("Starting debug process...")
|
| 14 |
+
|
| 15 |
+
# Step 1: Check if directories and files exist
|
| 16 |
+
print(f"Checking if processed_data directory exists: {PROCESSED_DATA_DIR.exists()}")
|
| 17 |
+
print(f"Checking if document_chunks.pkl exists: {CHUNKS_FILE.exists()}")
|
| 18 |
+
print(f"Checking if qdrant_vectorstore directory exists: {QDRANT_DIR.exists()}")
|
| 19 |
+
|
| 20 |
+
# Step 2: Try to load document chunks
|
| 21 |
+
try:
|
| 22 |
+
print("Attempting to load document chunks...")
|
| 23 |
+
with open(CHUNKS_FILE, 'rb') as f:
|
| 24 |
+
chunks = pickle.load(f)
|
| 25 |
+
print(f"Successfully loaded {len(chunks)} document chunks")
|
| 26 |
+
except Exception as e:
|
| 27 |
+
print(f"Error loading document chunks: {e}")
|
| 28 |
+
return
|
| 29 |
+
|
| 30 |
+
# Step 3: Try to initialize Qdrant client
|
| 31 |
+
try:
|
| 32 |
+
print(f"Attempting to initialize Qdrant client at {QDRANT_DIR}...")
|
| 33 |
+
client = QdrantClient(path=str(QDRANT_DIR))
|
| 34 |
+
|
| 35 |
+
# Try to list collections
|
| 36 |
+
collections = client.get_collections()
|
| 37 |
+
print(f"Available collections: {collections}")
|
| 38 |
+
|
| 39 |
+
# Try to get collection info
|
| 40 |
+
try:
|
| 41 |
+
collection_info = client.get_collection("kohavi_ab_testing_pdf_collection")
|
| 42 |
+
print(f"Collection info: {collection_info}")
|
| 43 |
+
except Exception as e:
|
| 44 |
+
print(f"Error getting collection info: {e}")
|
| 45 |
+
|
| 46 |
+
except Exception as e:
|
| 47 |
+
print(f"Error initializing Qdrant client: {e}")
|
| 48 |
+
return
|
| 49 |
+
|
| 50 |
+
print("Debug process completed successfully!")
|
| 51 |
+
|
| 52 |
+
if __name__ == "__main__":
|
| 53 |
+
main()
|
debug_output.log
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
2025-04-29 21:22:54 - Loaded .env file
|
debug_start.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import pickle
|
| 3 |
+
import sys
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from qdrant_client import QdrantClient
|
| 6 |
+
from langchain_openai.embeddings import OpenAIEmbeddings
|
| 7 |
+
from dotenv import load_dotenv
|
| 8 |
+
|
| 9 |
+
# Load .env file for OpenAI API key
|
| 10 |
+
load_dotenv()
|
| 11 |
+
print("Loaded .env file")
|
| 12 |
+
|
| 13 |
+
# Paths to pre-processed data
|
| 14 |
+
PROCESSED_DATA_DIR = Path("processed_data")
|
| 15 |
+
CHUNKS_FILE = PROCESSED_DATA_DIR / "document_chunks.pkl"
|
| 16 |
+
QDRANT_DIR = PROCESSED_DATA_DIR / "qdrant_vectorstore"
|
| 17 |
+
|
| 18 |
+
def main():
|
| 19 |
+
print("Starting debug process...")
|
| 20 |
+
|
| 21 |
+
# Step 1: Check if directories and files exist
|
| 22 |
+
print(f"Checking if processed_data directory exists: {PROCESSED_DATA_DIR.exists()}")
|
| 23 |
+
print(f"Checking if document_chunks.pkl exists: {CHUNKS_FILE.exists()}")
|
| 24 |
+
print(f"Checking if qdrant_vectorstore directory exists: {QDRANT_DIR.exists()}")
|
| 25 |
+
|
| 26 |
+
# Step 2: Try to load document chunks
|
| 27 |
+
try:
|
| 28 |
+
print("Attempting to load document chunks...")
|
| 29 |
+
with open(CHUNKS_FILE, 'rb') as f:
|
| 30 |
+
chunks = pickle.load(f)
|
| 31 |
+
print(f"Successfully loaded {len(chunks)} document chunks")
|
| 32 |
+
except Exception as e:
|
| 33 |
+
print(f"Error loading document chunks: {e}")
|
| 34 |
+
return
|
| 35 |
+
|
| 36 |
+
# Step 3: Try to initialize OpenAI embeddings
|
| 37 |
+
try:
|
| 38 |
+
print("Initializing OpenAI embeddings...")
|
| 39 |
+
embedding_model = OpenAIEmbeddings(model="text-embedding-3-small")
|
| 40 |
+
print("Successfully initialized OpenAI embeddings")
|
| 41 |
+
|
| 42 |
+
# Test with a simple query
|
| 43 |
+
test_embedding = embedding_model.embed_query("This is a test")
|
| 44 |
+
print(f"Successfully created test embedding with length {len(test_embedding)}")
|
| 45 |
+
except Exception as e:
|
| 46 |
+
print(f"Error initializing OpenAI embeddings: {e}")
|
| 47 |
+
return
|
| 48 |
+
|
| 49 |
+
# Step 4: Try to initialize Qdrant client
|
| 50 |
+
try:
|
| 51 |
+
print("Initializing Qdrant client...")
|
| 52 |
+
client = QdrantClient(path=str(QDRANT_DIR))
|
| 53 |
+
|
| 54 |
+
# List collections
|
| 55 |
+
collections = client.get_collections()
|
| 56 |
+
print(f"Available collections: {collections}")
|
| 57 |
+
|
| 58 |
+
# Get collection info
|
| 59 |
+
collection_name = "kohavi_ab_testing_pdf_collection"
|
| 60 |
+
collection_info = client.get_collection(collection_name)
|
| 61 |
+
print(f"Collection info: {collection_info}")
|
| 62 |
+
|
| 63 |
+
# Test a simple search
|
| 64 |
+
print("Testing search functionality...")
|
| 65 |
+
query_embedding = embedding_model.embed_query("What is AB testing?")
|
| 66 |
+
search_results = client.search(
|
| 67 |
+
collection_name=collection_name,
|
| 68 |
+
query_vector=query_embedding,
|
| 69 |
+
limit=2
|
| 70 |
+
)
|
| 71 |
+
print(f"Successfully retrieved {len(search_results)} search results")
|
| 72 |
+
|
| 73 |
+
# Display a sample result
|
| 74 |
+
if search_results:
|
| 75 |
+
print("First result ID:", search_results[0].id)
|
| 76 |
+
print("First result score:", search_results[0].score)
|
| 77 |
+
print("First result payload text (first 100 chars):", search_results[0].payload["text"][:100])
|
| 78 |
+
|
| 79 |
+
except Exception as e:
|
| 80 |
+
print(f"Error with Qdrant operations: {e}")
|
| 81 |
+
return
|
| 82 |
+
|
| 83 |
+
print("Debug process completed successfully!")
|
| 84 |
+
|
| 85 |
+
if __name__ == "__main__":
|
| 86 |
+
print("Script execution started")
|
| 87 |
+
main()
|
| 88 |
+
print("Script execution finished")
|
docker-compose.yml
CHANGED
|
@@ -10,6 +10,7 @@ services:
|
|
| 10 |
- "8000:8000"
|
| 11 |
volumes:
|
| 12 |
- ./data:/app/data
|
|
|
|
| 13 |
- ./.env:/app/.env
|
| 14 |
environment:
|
| 15 |
- OPENAI_API_KEY=${OPENAI_API_KEY}
|
|
|
|
| 10 |
- "8000:8000"
|
| 11 |
volumes:
|
| 12 |
- ./data:/app/data
|
| 13 |
+
- ./processed_data:/app/processed_data
|
| 14 |
- ./.env:/app/.env
|
| 15 |
environment:
|
| 16 |
- OPENAI_API_KEY=${OPENAI_API_KEY}
|
huggingface-space.yml
CHANGED
|
@@ -1,11 +1,22 @@
|
|
| 1 |
---
|
| 2 |
-
title: AB Testing RAG
|
| 3 |
emoji: ๐
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
license: mit
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
app_port: 8000
|
| 10 |
---
|
| 11 |
|
|
|
|
| 1 |
---
|
| 2 |
+
title: AB Testing RAG
|
| 3 |
emoji: ๐
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: indigo
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
license: mit
|
| 9 |
+
|
| 10 |
+
# Environment variables (fill these in on Hugging Face)
|
| 11 |
+
# OPENAI_API_KEY: Your OpenAI API Key
|
| 12 |
+
|
| 13 |
+
# Configuration for Dockerfile
|
| 14 |
+
dockerfile:
|
| 15 |
+
# Use environment variables to specify which app to run
|
| 16 |
+
env:
|
| 17 |
+
APP_MODE: streamlit # Options: streamlit or chainlit
|
| 18 |
+
|
| 19 |
+
# Streamlit app configuration
|
| 20 |
app_port: 8000
|
| 21 |
---
|
| 22 |
|
minimal_app.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import chainlit as cl
|
| 3 |
+
from dotenv import load_dotenv
|
| 4 |
+
|
| 5 |
+
# Load environment variables
|
| 6 |
+
load_dotenv()
|
| 7 |
+
print("Loaded .env file")
|
| 8 |
+
|
| 9 |
+
@cl.on_chat_start
|
| 10 |
+
async def start():
|
| 11 |
+
print("Chat started")
|
| 12 |
+
await cl.Message(content="Hello! I am a minimal AB Testing RAG agent.").send()
|
| 13 |
+
print("Welcome message sent")
|
| 14 |
+
|
| 15 |
+
@cl.on_message
|
| 16 |
+
async def main(message):
|
| 17 |
+
print(f"Received message: {message.content[:30]}...")
|
| 18 |
+
await cl.Message(content=f"You asked: {message.content}\n\nThis is a minimal test response.").send()
|
| 19 |
+
print("Response sent")
|
notebook_version/.DS_Store
CHANGED
|
Binary files a/notebook_version/.DS_Store and b/notebook_version/.DS_Store differ
|
|
|
requirements.txt
CHANGED
|
@@ -10,4 +10,6 @@ tiktoken==0.5.2
|
|
| 10 |
python-dotenv==1.0.1
|
| 11 |
unstructured==0.12.5
|
| 12 |
pypdf==3.17.4
|
| 13 |
-
numpy==1.26.3
|
|
|
|
|
|
|
|
|
| 10 |
python-dotenv==1.0.1
|
| 11 |
unstructured==0.12.5
|
| 12 |
pypdf==3.17.4
|
| 13 |
+
numpy==1.26.3
|
| 14 |
+
streamlit==1.32.0
|
| 15 |
+
requests==2.31.0
|
run_log.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
2025-04-29 21:21:30 - Loaded .env file
|
scripts/docker-entrypoint.sh
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
set -e
|
| 3 |
+
|
| 4 |
+
# Function to check if pre-processed data exists
|
| 5 |
+
check_preprocessed_data() {
|
| 6 |
+
if [ -d "/app/processed_data/qdrant_vectorstore" ] && [ -f "/app/processed_data/document_chunks.pkl" ]; then
|
| 7 |
+
return 0 # Data exists
|
| 8 |
+
else
|
| 9 |
+
return 1 # Data doesn't exist
|
| 10 |
+
fi
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
# Check if pre-processed data already exists
|
| 14 |
+
if check_preprocessed_data; then
|
| 15 |
+
echo "Pre-processed data already exists and will be used"
|
| 16 |
+
echo "No need to process PDF files as processed data is already available"
|
| 17 |
+
else
|
| 18 |
+
# Count PDF files in data directory
|
| 19 |
+
pdf_count=$(find /app/data -name "*.pdf" | wc -l)
|
| 20 |
+
echo "Found $pdf_count PDF files in data directory"
|
| 21 |
+
|
| 22 |
+
# If there are PDF files but no pre-processed data, run pre-processing
|
| 23 |
+
if [ "$pdf_count" -gt 0 ]; then
|
| 24 |
+
echo "Pre-processing PDF files..."
|
| 25 |
+
python /app/scripts/preprocess_data.py
|
| 26 |
+
echo "Pre-processing complete"
|
| 27 |
+
else
|
| 28 |
+
echo "WARNING: No PDF files found in data directory and no pre-processed data exists"
|
| 29 |
+
echo "The application will likely fail to start"
|
| 30 |
+
fi
|
| 31 |
+
fi
|
| 32 |
+
|
| 33 |
+
# Execute the provided command (usually starting the Chainlit app)
|
| 34 |
+
exec "$@"
|
scripts/prepare_for_deployment.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Prepares the repository for deployment to Hugging Face Spaces.
|
| 4 |
+
|
| 5 |
+
This script:
|
| 6 |
+
1. Verifies that pre-processed data exists
|
| 7 |
+
2. Checks that PDF files aren't included in the repository
|
| 8 |
+
3. Lists the files that will be included in the deployment
|
| 9 |
+
4. Provides instructions for deploying to Hugging Face
|
| 10 |
+
|
| 11 |
+
Run this after successfully pre-processing your data with preprocess_data.py
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
import os
|
| 15 |
+
import sys
|
| 16 |
+
from pathlib import Path
|
| 17 |
+
import shutil
|
| 18 |
+
|
| 19 |
+
PROCESSED_DATA_DIR = Path("processed_data")
|
| 20 |
+
CHUNKS_FILE = PROCESSED_DATA_DIR / "document_chunks.pkl"
|
| 21 |
+
QDRANT_DIR = PROCESSED_DATA_DIR / "qdrant_vectorstore"
|
| 22 |
+
DATA_DIR = Path("data")
|
| 23 |
+
|
| 24 |
+
def check_preprocessed_data():
|
| 25 |
+
"""Check if pre-processed data exists and is ready for deployment."""
|
| 26 |
+
print("\n=== Checking for pre-processed data ===")
|
| 27 |
+
|
| 28 |
+
if not PROCESSED_DATA_DIR.exists():
|
| 29 |
+
print(f"โ ERROR: Processed data directory not found: {PROCESSED_DATA_DIR}")
|
| 30 |
+
print(" Please run scripts/preprocess_data.py first.")
|
| 31 |
+
return False
|
| 32 |
+
|
| 33 |
+
if not CHUNKS_FILE.exists():
|
| 34 |
+
print(f"โ ERROR: Document chunks file not found: {CHUNKS_FILE}")
|
| 35 |
+
print(" Please run scripts/preprocess_data.py first.")
|
| 36 |
+
return False
|
| 37 |
+
|
| 38 |
+
if not QDRANT_DIR.exists():
|
| 39 |
+
print(f"โ ERROR: Vector store directory not found: {QDRANT_DIR}")
|
| 40 |
+
print(" Please run scripts/preprocess_data.py first.")
|
| 41 |
+
return False
|
| 42 |
+
|
| 43 |
+
print(f"โ
Found processed data directory: {PROCESSED_DATA_DIR}")
|
| 44 |
+
print(f"โ
Found document chunks file: {CHUNKS_FILE}")
|
| 45 |
+
print(f"โ
Found vector store directory: {QDRANT_DIR}")
|
| 46 |
+
|
| 47 |
+
# Check if vector store actually has content
|
| 48 |
+
qdrant_files = list(QDRANT_DIR.glob("**/*"))
|
| 49 |
+
if len(qdrant_files) < 5: # Arbitrary threshold for a minimum number of files
|
| 50 |
+
print(f"โ ๏ธ WARNING: Vector store directory might be empty or incomplete.")
|
| 51 |
+
print(f" Only found {len(qdrant_files)} files in {QDRANT_DIR}")
|
| 52 |
+
else:
|
| 53 |
+
print(f"โ
Vector store directory contains {len(qdrant_files)} files/directories")
|
| 54 |
+
|
| 55 |
+
return True
|
| 56 |
+
|
| 57 |
+
def check_for_pdf_files():
|
| 58 |
+
"""Check that PDF files aren't included in the data directory."""
|
| 59 |
+
print("\n=== Checking for PDF files ===")
|
| 60 |
+
|
| 61 |
+
pdf_files = list(DATA_DIR.glob("**/*.pdf"))
|
| 62 |
+
|
| 63 |
+
if pdf_files:
|
| 64 |
+
print(f"โ ๏ธ WARNING: Found {len(pdf_files)} PDF files in the data directory.")
|
| 65 |
+
print(" These files will NOT be committed to the repository if you follow the instructions below.")
|
| 66 |
+
print(" PDFs in the data directory are excluded in .gitignore.")
|
| 67 |
+
for pdf in pdf_files[:5]: # Show first 5 PDFs only
|
| 68 |
+
print(f" - {pdf}")
|
| 69 |
+
if len(pdf_files) > 5:
|
| 70 |
+
print(f" - ... and {len(pdf_files) - 5} more")
|
| 71 |
+
else:
|
| 72 |
+
print("โ
No PDF files found in the data directory - good!")
|
| 73 |
+
|
| 74 |
+
return True
|
| 75 |
+
|
| 76 |
+
def list_deployment_files():
|
| 77 |
+
"""List the essential files that will be included in the deployment."""
|
| 78 |
+
print("\n=== Files to include in deployment ===")
|
| 79 |
+
|
| 80 |
+
essential_files = [
|
| 81 |
+
"app.py",
|
| 82 |
+
"requirements.txt",
|
| 83 |
+
"Dockerfile",
|
| 84 |
+
"docker-compose.yml",
|
| 85 |
+
"chainlit.md",
|
| 86 |
+
"chainlit.json",
|
| 87 |
+
"README.md",
|
| 88 |
+
"scripts/docker-entrypoint.sh",
|
| 89 |
+
".dockerignore",
|
| 90 |
+
"processed_data/",
|
| 91 |
+
]
|
| 92 |
+
|
| 93 |
+
print("The following files and directories should be included in your deployment:")
|
| 94 |
+
for file in essential_files:
|
| 95 |
+
file_path = Path(file)
|
| 96 |
+
if file_path.exists() or (file.endswith('/') and Path(file.rstrip('/')).exists()):
|
| 97 |
+
print(f"โ
{file}")
|
| 98 |
+
else:
|
| 99 |
+
print(f"โ {file} - Not found!")
|
| 100 |
+
|
| 101 |
+
return True
|
| 102 |
+
|
| 103 |
+
def provide_deployment_instructions():
|
| 104 |
+
"""Provide instructions for deploying to Hugging Face."""
|
| 105 |
+
print("\n=== Deployment Instructions ===")
|
| 106 |
+
|
| 107 |
+
print("""
|
| 108 |
+
To deploy to Hugging Face Spaces:
|
| 109 |
+
|
| 110 |
+
1. Ensure you have the Hugging Face CLI installed:
|
| 111 |
+
pip install huggingface_hub
|
| 112 |
+
|
| 113 |
+
2. Log in to Hugging Face:
|
| 114 |
+
huggingface-cli login
|
| 115 |
+
|
| 116 |
+
3. Create a new Hugging Face Space with Docker deployment from the Hugging Face website
|
| 117 |
+
|
| 118 |
+
4. Add your repository as a remote:
|
| 119 |
+
git remote add huggingface https://huggingface.co/spaces/YOUR_USERNAME/YOUR_SPACE_NAME
|
| 120 |
+
|
| 121 |
+
5. Stage the necessary files (do NOT include PDF files):
|
| 122 |
+
git add app.py requirements.txt Dockerfile docker-compose.yml chainlit.md chainlit.json README.md scripts/docker-entrypoint.sh .dockerignore processed_data/
|
| 123 |
+
|
| 124 |
+
6. Commit the changes:
|
| 125 |
+
git commit -m "Prepare for Hugging Face deployment with pre-processed data"
|
| 126 |
+
|
| 127 |
+
7. Push to Hugging Face:
|
| 128 |
+
git push huggingface main
|
| 129 |
+
|
| 130 |
+
8. Set your OpenAI API key in the Hugging Face Space settings
|
| 131 |
+
""")
|
| 132 |
+
|
| 133 |
+
return True
|
| 134 |
+
|
| 135 |
+
def main():
|
| 136 |
+
"""Main entry point of the script."""
|
| 137 |
+
print("=" * 80)
|
| 138 |
+
print("PREPARING FOR HUGGING FACE DEPLOYMENT")
|
| 139 |
+
print("=" * 80)
|
| 140 |
+
|
| 141 |
+
checks = [
|
| 142 |
+
check_preprocessed_data,
|
| 143 |
+
check_for_pdf_files,
|
| 144 |
+
list_deployment_files,
|
| 145 |
+
provide_deployment_instructions
|
| 146 |
+
]
|
| 147 |
+
|
| 148 |
+
all_passed = True
|
| 149 |
+
for check in checks:
|
| 150 |
+
if not check():
|
| 151 |
+
all_passed = False
|
| 152 |
+
|
| 153 |
+
if all_passed:
|
| 154 |
+
print("\nโ
All checks passed! Your repository is ready for deployment to Hugging Face Spaces.")
|
| 155 |
+
print(" Follow the deployment instructions above to deploy your application.")
|
| 156 |
+
else:
|
| 157 |
+
print("\nโ Some checks failed. Please fix the issues before deploying.")
|
| 158 |
+
sys.exit(1)
|
| 159 |
+
|
| 160 |
+
if __name__ == "__main__":
|
| 161 |
+
main()
|
scripts/preprocess_data.py
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Pre-processes PDF files in the data directory, creating and saving:
|
| 4 |
+
1. Document chunks with metadata
|
| 5 |
+
2. Vector embeddings
|
| 6 |
+
|
| 7 |
+
This allows the app to load pre-processed data directly instead of processing
|
| 8 |
+
PDFs at runtime, making the app start faster and eliminating the need to
|
| 9 |
+
upload PDFs to Hugging Face.
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
import os
|
| 13 |
+
import pickle
|
| 14 |
+
import tiktoken
|
| 15 |
+
from pathlib import Path
|
| 16 |
+
from collections import defaultdict
|
| 17 |
+
import json
|
| 18 |
+
|
| 19 |
+
# Import required LangChain modules
|
| 20 |
+
from langchain_community.document_loaders import DirectoryLoader
|
| 21 |
+
from langchain_community.document_loaders import PyPDFLoader
|
| 22 |
+
from langchain.text_splitter import RecursiveCharacterTextSplitter
|
| 23 |
+
from langchain_openai.embeddings import OpenAIEmbeddings
|
| 24 |
+
from langchain_qdrant import Qdrant
|
| 25 |
+
from langchain_core.documents import Document
|
| 26 |
+
from qdrant_client import QdrantClient
|
| 27 |
+
from qdrant_client.models import Distance, VectorParams, PointStruct
|
| 28 |
+
|
| 29 |
+
# Set OpenAI API key
|
| 30 |
+
from dotenv import load_dotenv
|
| 31 |
+
load_dotenv()
|
| 32 |
+
|
| 33 |
+
# Ensure OpenAI API key is available
|
| 34 |
+
if not os.environ.get("OPENAI_API_KEY"):
|
| 35 |
+
raise EnvironmentError("OPENAI_API_KEY environment variable not found. Please set it in your .env file.")
|
| 36 |
+
|
| 37 |
+
# Create directories to store pre-processed data
|
| 38 |
+
PROCESSED_DATA_DIR = Path("processed_data")
|
| 39 |
+
PROCESSED_DATA_DIR.mkdir(exist_ok=True)
|
| 40 |
+
|
| 41 |
+
CHUNKS_FILE = PROCESSED_DATA_DIR / "document_chunks.pkl"
|
| 42 |
+
QDRANT_DIR = PROCESSED_DATA_DIR / "qdrant_vectorstore"
|
| 43 |
+
|
| 44 |
+
def load_and_process_documents():
|
| 45 |
+
"""
|
| 46 |
+
Load PDF documents, merge them by source, and split into chunks.
|
| 47 |
+
This is the same process used in the notebook and app.py.
|
| 48 |
+
"""
|
| 49 |
+
print("Loading PDF documents...")
|
| 50 |
+
path = "data/"
|
| 51 |
+
loader = DirectoryLoader(path, glob="*.pdf", loader_cls=PyPDFLoader)
|
| 52 |
+
all_docs = loader.load()
|
| 53 |
+
print(f"Loaded {len(all_docs)} pages from PDF files")
|
| 54 |
+
|
| 55 |
+
# Create a mapping of merged document chunks back to original pages
|
| 56 |
+
page_map = {}
|
| 57 |
+
current_index = 0
|
| 58 |
+
|
| 59 |
+
# For source tracking, we'll store page information before merging
|
| 60 |
+
docs_by_source = defaultdict(list)
|
| 61 |
+
|
| 62 |
+
# Group documents by their source file
|
| 63 |
+
for doc in all_docs:
|
| 64 |
+
source = doc.metadata.get("source", "")
|
| 65 |
+
docs_by_source[source].append(doc)
|
| 66 |
+
|
| 67 |
+
# Merge pages from the same PDF but track page ranges
|
| 68 |
+
merged_docs = []
|
| 69 |
+
for source, source_docs in docs_by_source.items():
|
| 70 |
+
# Sort by page number if available
|
| 71 |
+
source_docs.sort(key=lambda x: x.metadata.get("page", 0))
|
| 72 |
+
|
| 73 |
+
# Merge the content
|
| 74 |
+
merged_content = ""
|
| 75 |
+
page_ranges = []
|
| 76 |
+
current_pos = 0
|
| 77 |
+
|
| 78 |
+
for doc in source_docs:
|
| 79 |
+
# Get the page number (1-indexed for human readability)
|
| 80 |
+
page_num = doc.metadata.get("page", 0) + 1
|
| 81 |
+
|
| 82 |
+
# Add a separator between pages for clarity
|
| 83 |
+
if merged_content:
|
| 84 |
+
merged_content += "\n\n"
|
| 85 |
+
|
| 86 |
+
# Record where this page's content starts in the merged document
|
| 87 |
+
start_pos = len(merged_content)
|
| 88 |
+
merged_content += doc.page_content
|
| 89 |
+
end_pos = len(merged_content)
|
| 90 |
+
|
| 91 |
+
# Store the mapping of character ranges to original page numbers
|
| 92 |
+
page_ranges.append({
|
| 93 |
+
"start": start_pos,
|
| 94 |
+
"end": end_pos,
|
| 95 |
+
"page": page_num,
|
| 96 |
+
"source": source
|
| 97 |
+
})
|
| 98 |
+
|
| 99 |
+
# Create merged metadata that includes page mapping information
|
| 100 |
+
merged_metadata = {
|
| 101 |
+
"source": source,
|
| 102 |
+
"title": source.split("/")[-1],
|
| 103 |
+
"page_count": len(source_docs),
|
| 104 |
+
"merged": True,
|
| 105 |
+
"page_ranges": page_ranges # Store the page ranges for later reference
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
# Create a new document with the merged content
|
| 109 |
+
merged_doc = Document(page_content=merged_content, metadata=merged_metadata)
|
| 110 |
+
merged_docs.append(merged_doc)
|
| 111 |
+
|
| 112 |
+
print(f"Created {len(merged_docs)} merged documents")
|
| 113 |
+
|
| 114 |
+
# tiktoken_len counts tokens (not characters) using the gpt-4o-mini tokenizer
|
| 115 |
+
def tiktoken_len(text):
|
| 116 |
+
tokens = tiktoken.encoding_for_model("gpt-4o-mini").encode(
|
| 117 |
+
text,
|
| 118 |
+
)
|
| 119 |
+
return len(tokens)
|
| 120 |
+
|
| 121 |
+
# Process splits to add page info based on character position
|
| 122 |
+
def add_page_info_to_splits(splits):
|
| 123 |
+
for split in splits:
|
| 124 |
+
# Get the start position of this chunk
|
| 125 |
+
start_pos = split.metadata.get("start_index", 0)
|
| 126 |
+
end_pos = start_pos + len(split.page_content)
|
| 127 |
+
|
| 128 |
+
# Find which page this chunk belongs to
|
| 129 |
+
if "page_ranges" in split.metadata:
|
| 130 |
+
for page_range in split.metadata["page_ranges"]:
|
| 131 |
+
# If chunk significantly overlaps with this page range
|
| 132 |
+
if (start_pos <= page_range["end"] and
|
| 133 |
+
end_pos >= page_range["start"]):
|
| 134 |
+
# Use this page number
|
| 135 |
+
split.metadata["page"] = page_range["page"]
|
| 136 |
+
break
|
| 137 |
+
return splits
|
| 138 |
+
|
| 139 |
+
# Split the text with start index tracking
|
| 140 |
+
print("Splitting documents into chunks...")
|
| 141 |
+
text_splitter = RecursiveCharacterTextSplitter(
|
| 142 |
+
chunk_size=300,
|
| 143 |
+
chunk_overlap=50,
|
| 144 |
+
length_function=tiktoken_len,
|
| 145 |
+
add_start_index=True
|
| 146 |
+
)
|
| 147 |
+
|
| 148 |
+
# Split and then process to add page information
|
| 149 |
+
raw_splits = text_splitter.split_documents(merged_docs)
|
| 150 |
+
split_chunks = add_page_info_to_splits(raw_splits)
|
| 151 |
+
print(f"Created {len(split_chunks)} document chunks")
|
| 152 |
+
|
| 153 |
+
return split_chunks
|
| 154 |
+
|
| 155 |
+
def create_and_save_vectorstore(chunks):
|
| 156 |
+
"""
|
| 157 |
+
Create a vector store from document chunks and save it to disk.
|
| 158 |
+
"""
|
| 159 |
+
print("Creating embeddings and vector store...")
|
| 160 |
+
embedding_model = OpenAIEmbeddings(model="text-embedding-3-small")
|
| 161 |
+
|
| 162 |
+
# Extract text and metadata for separate processing
|
| 163 |
+
texts = [doc.page_content for doc in chunks]
|
| 164 |
+
metadatas = [doc.metadata for doc in chunks]
|
| 165 |
+
|
| 166 |
+
# Ensure the directory exists
|
| 167 |
+
QDRANT_DIR.mkdir(exist_ok=True, parents=True)
|
| 168 |
+
|
| 169 |
+
# Create a local Qdrant client
|
| 170 |
+
client = QdrantClient(path=str(QDRANT_DIR))
|
| 171 |
+
|
| 172 |
+
# Get the embedding dimension
|
| 173 |
+
sample_embedding = embedding_model.embed_query("Sample text")
|
| 174 |
+
|
| 175 |
+
# Create the collection if it doesn't exist
|
| 176 |
+
collection_name = "kohavi_ab_testing_pdf_collection"
|
| 177 |
+
try:
|
| 178 |
+
collection_info = client.get_collection(collection_name)
|
| 179 |
+
print(f"Collection {collection_name} already exists")
|
| 180 |
+
except Exception:
|
| 181 |
+
# Collection doesn't exist, create it
|
| 182 |
+
print(f"Creating collection {collection_name}")
|
| 183 |
+
client.create_collection(
|
| 184 |
+
collection_name=collection_name,
|
| 185 |
+
vectors_config=VectorParams(
|
| 186 |
+
size=len(sample_embedding),
|
| 187 |
+
distance=Distance.COSINE
|
| 188 |
+
)
|
| 189 |
+
)
|
| 190 |
+
|
| 191 |
+
# Process in batches to avoid memory issues
|
| 192 |
+
batch_size = 100
|
| 193 |
+
print(f"Processing {len(texts)} documents in batches of {batch_size}")
|
| 194 |
+
|
| 195 |
+
for i in range(0, len(texts), batch_size):
|
| 196 |
+
batch_texts = texts[i:i+batch_size]
|
| 197 |
+
batch_metadatas = metadatas[i:i+batch_size]
|
| 198 |
+
|
| 199 |
+
print(f"Processing batch {i//batch_size + 1}/{(len(texts) + batch_size - 1)//batch_size}")
|
| 200 |
+
|
| 201 |
+
# Get embeddings for this batch
|
| 202 |
+
embeddings = embedding_model.embed_documents(batch_texts)
|
| 203 |
+
|
| 204 |
+
# Create points for this batch
|
| 205 |
+
points = []
|
| 206 |
+
for j, (text, embedding, metadata) in enumerate(zip(batch_texts, embeddings, batch_metadatas)):
|
| 207 |
+
points.append(PointStruct(
|
| 208 |
+
id=i + j,
|
| 209 |
+
vector=embedding,
|
| 210 |
+
payload={
|
| 211 |
+
"text": text,
|
| 212 |
+
"metadata": metadata
|
| 213 |
+
}
|
| 214 |
+
))
|
| 215 |
+
|
| 216 |
+
# Upsert points into the collection
|
| 217 |
+
client.upsert(
|
| 218 |
+
collection_name=collection_name,
|
| 219 |
+
points=points
|
| 220 |
+
)
|
| 221 |
+
|
| 222 |
+
print(f"Vector store created and saved to {QDRANT_DIR}")
|
| 223 |
+
return True
|
| 224 |
+
|
| 225 |
+
def main():
|
| 226 |
+
# Load and process documents
|
| 227 |
+
print("Starting pre-processing of PDF files...")
|
| 228 |
+
chunks = load_and_process_documents()
|
| 229 |
+
|
| 230 |
+
# Save chunks to disk
|
| 231 |
+
print(f"Saving {len(chunks)} document chunks to {CHUNKS_FILE}...")
|
| 232 |
+
with open(CHUNKS_FILE, 'wb') as f:
|
| 233 |
+
pickle.dump(chunks, f)
|
| 234 |
+
print(f"Chunks saved to {CHUNKS_FILE}")
|
| 235 |
+
|
| 236 |
+
# Create and save vector store
|
| 237 |
+
success = create_and_save_vectorstore(chunks)
|
| 238 |
+
|
| 239 |
+
if success:
|
| 240 |
+
print("Pre-processing complete! The application can now use these pre-processed files.")
|
| 241 |
+
print(f"- Document chunks: {CHUNKS_FILE}")
|
| 242 |
+
print(f"- Vector store: {QDRANT_DIR}")
|
| 243 |
+
else:
|
| 244 |
+
print("Error creating vector store. Please check the logs.")
|
| 245 |
+
|
| 246 |
+
if __name__ == "__main__":
|
| 247 |
+
main()
|
streamlit_app.py
ADDED
|
@@ -0,0 +1,476 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import pickle
|
| 3 |
+
import streamlit as st
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from dotenv import load_dotenv
|
| 6 |
+
from langchain_openai.chat_models import ChatOpenAI
|
| 7 |
+
from langchain_openai.embeddings import OpenAIEmbeddings
|
| 8 |
+
from langchain_core.prompts import ChatPromptTemplate
|
| 9 |
+
from qdrant_client import QdrantClient
|
| 10 |
+
from langchain_core.documents import Document
|
| 11 |
+
from langchain.agents import AgentExecutor, create_openai_tools_agent
|
| 12 |
+
from langchain_core.tools import tool
|
| 13 |
+
from langchain.agents.format_scratchpad.openai_tools import format_to_openai_tool_messages
|
| 14 |
+
from langchain_core.messages import AIMessage, HumanMessage
|
| 15 |
+
import requests
|
| 16 |
+
import json
|
| 17 |
+
from langchain_core.output_parsers import StrOutputParser
|
| 18 |
+
|
| 19 |
+
# Global variable to store ArXiv sources
|
| 20 |
+
ARXIV_SOURCES = []
|
| 21 |
+
|
| 22 |
+
# Load environment variables
|
| 23 |
+
load_dotenv()
|
| 24 |
+
print("Loaded .env file")
|
| 25 |
+
|
| 26 |
+
# Configure OpenAI API key from environment variable
|
| 27 |
+
if not os.environ.get("OPENAI_API_KEY"):
|
| 28 |
+
os.environ["OPENAI_API_KEY"] = os.environ.get("OPENAI_API_KEY_BACKUP", "")
|
| 29 |
+
|
| 30 |
+
# Paths to pre-processed data
|
| 31 |
+
PROCESSED_DATA_DIR = Path("processed_data")
|
| 32 |
+
CHUNKS_FILE = PROCESSED_DATA_DIR / "document_chunks.pkl"
|
| 33 |
+
QDRANT_DIR = PROCESSED_DATA_DIR / "qdrant_vectorstore"
|
| 34 |
+
|
| 35 |
+
# Define prompts
|
| 36 |
+
INITIAL_RAG_PROMPT = """
|
| 37 |
+
CONTEXT:
|
| 38 |
+
{context}
|
| 39 |
+
|
| 40 |
+
QUERY:
|
| 41 |
+
{question}
|
| 42 |
+
|
| 43 |
+
You are a helpful assistant with expertise in AB Testing. Use the available context to answer the question. Do not use your own knowledge! If you cannot answer the question based on the context, you must say "I don't know, but I can try a different approach if you'd like."
|
| 44 |
+
"""
|
| 45 |
+
|
| 46 |
+
EVALUATE_RESPONSE_PROMPT = """
|
| 47 |
+
QUERY:
|
| 48 |
+
{question}
|
| 49 |
+
|
| 50 |
+
RESPONSE:
|
| 51 |
+
{response}
|
| 52 |
+
|
| 53 |
+
Evaluate if the response sufficiently answers the query based on the following criteria:
|
| 54 |
+
1. Relevance: Does the response directly address the query topic?
|
| 55 |
+
2. Completeness: Does the response fully answer all aspects of the query?
|
| 56 |
+
3. Accuracy: Is the information provided factually correct and helpful?
|
| 57 |
+
|
| 58 |
+
Return only "SUFFICIENT" if the response meets all criteria, or "INSUFFICIENT" if the response needs improvement.
|
| 59 |
+
"""
|
| 60 |
+
|
| 61 |
+
REPHRASE_QUERY_PROMPT = """
|
| 62 |
+
QUERY:
|
| 63 |
+
{question}
|
| 64 |
+
|
| 65 |
+
You are a helpful assistant. Rephrase the provided query to be more specific and to the point in order to improve retrieval in our RAG pipeline about AB Testing.
|
| 66 |
+
"""
|
| 67 |
+
|
| 68 |
+
AGENT_PROMPT = """
|
| 69 |
+
You are an expert AB Testing assistant. Your job is to provide helpful, accurate information about AB Testing topics.
|
| 70 |
+
|
| 71 |
+
You have access to several tools:
|
| 72 |
+
1. You can search for relevant documents in the database using search_documents - use this for general AB testing questions
|
| 73 |
+
2. You can rephrase a query to get better search results using search_with_rephrased_query - use this when initial searches don't yield good results
|
| 74 |
+
3. You can search ArXiv for academic papers using search_arxiv - use this for:
|
| 75 |
+
a) Specific academic papers, their authors, or publications
|
| 76 |
+
b) As a fallback when other tools don't yield satisfactory results
|
| 77 |
+
c) Technical questions that might be better answered with academic research
|
| 78 |
+
|
| 79 |
+
When the user asks about specific papers, authors of papers, or academic publications, you should IMMEDIATELY use the search_arxiv tool rather than the document search tools.
|
| 80 |
+
|
| 81 |
+
For general AB testing questions, follow this process:
|
| 82 |
+
1. First try search_documents
|
| 83 |
+
2. If that doesn't provide good information, try search_with_rephrased_query
|
| 84 |
+
3. If still insufficient, try search_arxiv as a final resource before giving up
|
| 85 |
+
|
| 86 |
+
Use these tools to provide the best possible answer.
|
| 87 |
+
"""
|
| 88 |
+
|
| 89 |
+
@st.cache_resource
|
| 90 |
+
def load_document_chunks():
|
| 91 |
+
"""Load pre-processed document chunks from disk."""
|
| 92 |
+
with open(CHUNKS_FILE, 'rb') as f:
|
| 93 |
+
return pickle.load(f)
|
| 94 |
+
|
| 95 |
+
@st.cache_resource
|
| 96 |
+
def get_chat_model():
|
| 97 |
+
"""Get the chat model for initial RAG."""
|
| 98 |
+
return ChatOpenAI(
|
| 99 |
+
model="gpt-4.1-mini",
|
| 100 |
+
temperature=0,
|
| 101 |
+
)
|
| 102 |
+
|
| 103 |
+
@st.cache_resource
|
| 104 |
+
def get_agent_model():
|
| 105 |
+
"""Get the more powerful model for agent and evaluation."""
|
| 106 |
+
return ChatOpenAI(
|
| 107 |
+
model="gpt-4.1",
|
| 108 |
+
temperature=0,
|
| 109 |
+
)
|
| 110 |
+
|
| 111 |
+
@st.cache_resource
|
| 112 |
+
def get_embedding_model():
|
| 113 |
+
"""Get the embedding model."""
|
| 114 |
+
return OpenAIEmbeddings(model="text-embedding-3-small")
|
| 115 |
+
|
| 116 |
+
@st.cache_resource
|
| 117 |
+
def setup_qdrant_client():
|
| 118 |
+
"""Set up the Qdrant client."""
|
| 119 |
+
return QdrantClient(path=str(QDRANT_DIR))
|
| 120 |
+
|
| 121 |
+
def retrieve_documents(query, k=5):
|
| 122 |
+
"""Retrieve relevant documents for a query."""
|
| 123 |
+
# Get models and data
|
| 124 |
+
embedding_model = get_embedding_model()
|
| 125 |
+
chunks = load_document_chunks()
|
| 126 |
+
client = setup_qdrant_client()
|
| 127 |
+
|
| 128 |
+
# Create a mapping of IDs to documents
|
| 129 |
+
docs_by_id = {i: doc for i, doc in enumerate(chunks)}
|
| 130 |
+
|
| 131 |
+
# Get query embedding
|
| 132 |
+
query_embedding = embedding_model.embed_query(query)
|
| 133 |
+
|
| 134 |
+
# Search Qdrant
|
| 135 |
+
results = client.search(
|
| 136 |
+
collection_name="kohavi_ab_testing_pdf_collection",
|
| 137 |
+
query_vector=query_embedding,
|
| 138 |
+
limit=k
|
| 139 |
+
)
|
| 140 |
+
|
| 141 |
+
# Convert results to documents
|
| 142 |
+
documents = []
|
| 143 |
+
sources_dict = {} # Use a dictionary to track unique sources by file+page
|
| 144 |
+
|
| 145 |
+
print(f"Retrieved {len(results)} search results")
|
| 146 |
+
|
| 147 |
+
for result in results:
|
| 148 |
+
doc_id = result.id
|
| 149 |
+
if doc_id in docs_by_id:
|
| 150 |
+
doc = docs_by_id[doc_id]
|
| 151 |
+
documents.append(doc)
|
| 152 |
+
|
| 153 |
+
# Debug the metadata
|
| 154 |
+
print(f"Document metadata: {doc.metadata}")
|
| 155 |
+
|
| 156 |
+
# Extract source info
|
| 157 |
+
source_path = doc.metadata.get("source", "")
|
| 158 |
+
filename = source_path.split("/")[-1] if "/" in source_path else source_path
|
| 159 |
+
|
| 160 |
+
# Remove .pdf extension if present
|
| 161 |
+
if filename.lower().endswith('.pdf'):
|
| 162 |
+
filename = filename[:-4]
|
| 163 |
+
|
| 164 |
+
# Default to the full filename if we can't extract a title
|
| 165 |
+
if not filename:
|
| 166 |
+
filename = "Unknown Source"
|
| 167 |
+
|
| 168 |
+
# Get page number, use a default if not available
|
| 169 |
+
page = doc.metadata.get("page", "unknown")
|
| 170 |
+
|
| 171 |
+
# All PDF sources in data directory are by Ron Kohavi, so add his name as prefix
|
| 172 |
+
title = f"Ron Kohavi: {filename}"
|
| 173 |
+
|
| 174 |
+
# Create a unique key for this source based on filename and page
|
| 175 |
+
source_key = f"{filename}_{page}"
|
| 176 |
+
|
| 177 |
+
# Only add to sources if we haven't seen this exact source (same file, same page) before
|
| 178 |
+
if source_key not in sources_dict:
|
| 179 |
+
sources_dict[source_key] = {
|
| 180 |
+
"title": title,
|
| 181 |
+
"page": page,
|
| 182 |
+
"score": float(result.score),
|
| 183 |
+
"type": "pdf"
|
| 184 |
+
}
|
| 185 |
+
print(f"Added source: {title}, Page: {page}")
|
| 186 |
+
else:
|
| 187 |
+
print(f"Skipping duplicate source: {title}, Page: {page}")
|
| 188 |
+
|
| 189 |
+
# Convert the dictionary of unique sources back to a list
|
| 190 |
+
sources = list(sources_dict.values())
|
| 191 |
+
|
| 192 |
+
print(f"Returning {len(documents)} documents with {len(sources)} unique sources")
|
| 193 |
+
return documents, sources
|
| 194 |
+
|
| 195 |
+
def rephrase_query(query):
|
| 196 |
+
"""Rephrase the query to improve retrieval."""
|
| 197 |
+
chat_model = get_chat_model()
|
| 198 |
+
prompt = ChatPromptTemplate.from_template(REPHRASE_QUERY_PROMPT)
|
| 199 |
+
messages = prompt.format_messages(question=query)
|
| 200 |
+
response = chat_model.invoke(messages)
|
| 201 |
+
return response.content
|
| 202 |
+
|
| 203 |
+
def generate_answer(context, question):
|
| 204 |
+
"""Generate an answer using the context and question."""
|
| 205 |
+
chat_model = get_chat_model()
|
| 206 |
+
prompt = ChatPromptTemplate.from_template(INITIAL_RAG_PROMPT)
|
| 207 |
+
messages = prompt.format_messages(context=context, question=question)
|
| 208 |
+
response = chat_model.invoke(messages)
|
| 209 |
+
return response.content
|
| 210 |
+
|
| 211 |
+
def evaluate_response(question, response):
|
| 212 |
+
"""Evaluate if the response is sufficient."""
|
| 213 |
+
# Check if this is likely a question about academic papers or authors
|
| 214 |
+
paper_keywords = ["author", "paper", "publication", "published", "journal", "conference", "researchers", "wrote"]
|
| 215 |
+
|
| 216 |
+
is_paper_query = any(keyword in question.lower() for keyword in paper_keywords) and ("'" in question or '"' in question)
|
| 217 |
+
|
| 218 |
+
# If it's a paper query and the response indicates lack of knowledge, mark as insufficient
|
| 219 |
+
if is_paper_query and ("don't know" in response.lower() or "cannot " in response.lower() or "i don't have" in response.lower()):
|
| 220 |
+
print("Paper-specific query detected with insufficient answer, sending to agent")
|
| 221 |
+
return False
|
| 222 |
+
|
| 223 |
+
# Otherwise, use the LLM evaluation
|
| 224 |
+
agent_model = get_agent_model()
|
| 225 |
+
prompt = ChatPromptTemplate.from_template(EVALUATE_RESPONSE_PROMPT)
|
| 226 |
+
messages = prompt.format_messages(question=question, response=response)
|
| 227 |
+
result = agent_model.invoke(messages)
|
| 228 |
+
return "SUFFICIENT" in result.content
|
| 229 |
+
|
| 230 |
+
@tool
|
| 231 |
+
def search_documents(query: str) -> str:
|
| 232 |
+
"""Search for relevant documents in the AB Testing database."""
|
| 233 |
+
documents, _ = retrieve_documents(query)
|
| 234 |
+
if not documents:
|
| 235 |
+
return "No relevant documents found"
|
| 236 |
+
return "\n\n".join([doc.page_content for doc in documents])
|
| 237 |
+
|
| 238 |
+
@tool
|
| 239 |
+
def search_with_rephrased_query(query: str) -> str:
|
| 240 |
+
"""Rephrase the query and then search for relevant documents."""
|
| 241 |
+
rephrased = rephrase_query(query)
|
| 242 |
+
documents, _ = retrieve_documents(rephrased)
|
| 243 |
+
if not documents:
|
| 244 |
+
return "No relevant documents found even with rephrased query"
|
| 245 |
+
return "\n\n".join([doc.page_content for doc in documents])
|
| 246 |
+
|
| 247 |
+
@tool
|
| 248 |
+
def search_arxiv(query: str) -> str:
|
| 249 |
+
"""Search ArXiv for academic papers related to the query."""
|
| 250 |
+
global ARXIV_SOURCES
|
| 251 |
+
ARXIV_SOURCES = [] # Reset sources for new search
|
| 252 |
+
|
| 253 |
+
try:
|
| 254 |
+
# Check if the query is looking for a specific paper by title
|
| 255 |
+
if "paper" in query.lower() and ("title" in query.lower() or "called" in query.lower() or "named" in query.lower() or "'" in query or '"' in query):
|
| 256 |
+
# Try to extract paper title from quotes if present
|
| 257 |
+
import re
|
| 258 |
+
title_match = re.search(r'[\'"]([^\'"]+)[\'"]', query)
|
| 259 |
+
|
| 260 |
+
if title_match:
|
| 261 |
+
paper_title = title_match.group(1)
|
| 262 |
+
# Use title-specific search with exact match
|
| 263 |
+
formatted_query = f'ti:"{paper_title}"'
|
| 264 |
+
else:
|
| 265 |
+
# Fall back to general search but optimize for title
|
| 266 |
+
formatted_query = query.replace(' ', '+')
|
| 267 |
+
formatted_query = f'all:{formatted_query}'
|
| 268 |
+
else:
|
| 269 |
+
# General query
|
| 270 |
+
formatted_query = query.replace(' ', '+')
|
| 271 |
+
formatted_query = f'all:{formatted_query}'
|
| 272 |
+
|
| 273 |
+
print(f"Searching ArXiv with query: {formatted_query}")
|
| 274 |
+
url = f"http://export.arxiv.org/api/query?search_query={formatted_query}&start=0&max_results=5"
|
| 275 |
+
|
| 276 |
+
response = requests.get(url)
|
| 277 |
+
if response.status_code != 200:
|
| 278 |
+
return "Error fetching data from ArXiv"
|
| 279 |
+
|
| 280 |
+
# Parse response
|
| 281 |
+
import xml.etree.ElementTree as ET
|
| 282 |
+
root = ET.fromstring(response.text)
|
| 283 |
+
|
| 284 |
+
results = []
|
| 285 |
+
ns = {'atom': 'http://www.w3.org/2005/Atom'}
|
| 286 |
+
|
| 287 |
+
# Count total entries
|
| 288 |
+
total_entries = len(root.findall('atom:entry', ns))
|
| 289 |
+
print(f"Found {total_entries} papers on ArXiv")
|
| 290 |
+
|
| 291 |
+
# Clear previous sources and add new ones
|
| 292 |
+
ARXIV_SOURCES.clear()
|
| 293 |
+
|
| 294 |
+
for entry in root.findall('atom:entry', ns):
|
| 295 |
+
title = entry.find('atom:title', ns).text
|
| 296 |
+
authors = [author.find('atom:name', ns).text for author in entry.findall('atom:author', ns)]
|
| 297 |
+
summary = entry.find('atom:summary', ns).text
|
| 298 |
+
link = entry.find('atom:id', ns).text
|
| 299 |
+
|
| 300 |
+
# Add to global sources list
|
| 301 |
+
ARXIV_SOURCES.append({
|
| 302 |
+
"title": title,
|
| 303 |
+
"authors": ", ".join(authors),
|
| 304 |
+
"type": "arxiv"
|
| 305 |
+
})
|
| 306 |
+
|
| 307 |
+
results.append({
|
| 308 |
+
"title": title,
|
| 309 |
+
"authors": ", ".join(authors),
|
| 310 |
+
"summary": summary,
|
| 311 |
+
"link": link
|
| 312 |
+
})
|
| 313 |
+
|
| 314 |
+
if not results:
|
| 315 |
+
return "No papers found on ArXiv matching the query"
|
| 316 |
+
|
| 317 |
+
# Format results as text
|
| 318 |
+
text_results = []
|
| 319 |
+
for i, paper in enumerate(results):
|
| 320 |
+
text_results.append(f"Paper {i+1}:\nTitle: {paper['title']}\nAuthors: {paper['authors']}\nSummary: {paper['summary'][:300]}...\nLink: {paper['link']}\n")
|
| 321 |
+
|
| 322 |
+
return "\n".join(text_results)
|
| 323 |
+
except Exception as e:
|
| 324 |
+
return f"Error searching ArXiv: {str(e)}"
|
| 325 |
+
|
| 326 |
+
def setup_agent():
|
| 327 |
+
"""Set up the agent with tools."""
|
| 328 |
+
agent_model = get_agent_model()
|
| 329 |
+
tools = [search_documents, search_with_rephrased_query, search_arxiv]
|
| 330 |
+
prompt = ChatPromptTemplate.from_messages([
|
| 331 |
+
("system", AGENT_PROMPT),
|
| 332 |
+
("human", "{input}"),
|
| 333 |
+
("ai", "{agent_scratchpad}")
|
| 334 |
+
])
|
| 335 |
+
|
| 336 |
+
agent = create_openai_tools_agent(agent_model, tools, prompt)
|
| 337 |
+
executor = AgentExecutor(
|
| 338 |
+
agent=agent,
|
| 339 |
+
tools=tools,
|
| 340 |
+
verbose=True,
|
| 341 |
+
handle_parsing_errors=True
|
| 342 |
+
)
|
| 343 |
+
|
| 344 |
+
return executor
|
| 345 |
+
|
| 346 |
+
# Streamlit UI
|
| 347 |
+
st.set_page_config(
|
| 348 |
+
page_title="AB Testing RAG Agent",
|
| 349 |
+
page_icon="๐",
|
| 350 |
+
layout="wide"
|
| 351 |
+
)
|
| 352 |
+
|
| 353 |
+
st.title("๐ AB Testing RAG Agent")
|
| 354 |
+
st.markdown("""
|
| 355 |
+
This specialized agent can answer questions about A/B Testing using a collection of Ron Kohavi's work. If it can't fully answer your A/B Testing questions using this collection, it will then automatically search Arxiv. Let's begin!
|
| 356 |
+
""")
|
| 357 |
+
|
| 358 |
+
# Initialize chat history
|
| 359 |
+
if "messages" not in st.session_state:
|
| 360 |
+
st.session_state.messages = []
|
| 361 |
+
|
| 362 |
+
# Display chat history
|
| 363 |
+
for message in st.session_state.messages:
|
| 364 |
+
with st.chat_message(message["role"]):
|
| 365 |
+
st.markdown(message["content"])
|
| 366 |
+
|
| 367 |
+
# Display sources if available
|
| 368 |
+
if "sources" in message and message["sources"]:
|
| 369 |
+
st.markdown("#### Sources")
|
| 370 |
+
for i, source in enumerate(message["sources"]):
|
| 371 |
+
title = source.get("title", "Unknown")
|
| 372 |
+
|
| 373 |
+
# Display differently based on source type
|
| 374 |
+
if source.get("type") == "arxiv":
|
| 375 |
+
authors = source.get("authors", "Unknown authors")
|
| 376 |
+
st.markdown(f"**{i+1}. {title}**\nAuthors: {authors}")
|
| 377 |
+
else:
|
| 378 |
+
# PDF source with page number
|
| 379 |
+
page = source.get("page", "Unknown")
|
| 380 |
+
st.markdown(f"**{i+1}. {title}** (Page: {page})")
|
| 381 |
+
|
| 382 |
+
# Input for new question
|
| 383 |
+
query = st.chat_input("Ask a question about A/B Testing")
|
| 384 |
+
|
| 385 |
+
if query:
|
| 386 |
+
# Add user message to chat history
|
| 387 |
+
st.session_state.messages.append({"role": "user", "content": query})
|
| 388 |
+
|
| 389 |
+
# Display user message
|
| 390 |
+
with st.chat_message("user"):
|
| 391 |
+
st.markdown(query)
|
| 392 |
+
|
| 393 |
+
# Display assistant response
|
| 394 |
+
with st.chat_message("assistant"):
|
| 395 |
+
message_placeholder = st.empty()
|
| 396 |
+
|
| 397 |
+
with st.status("Processing your query...", expanded=True) as status:
|
| 398 |
+
# Try initial RAG approach first (matching original architecture)
|
| 399 |
+
st.write("Attempting initial search...")
|
| 400 |
+
documents, sources = retrieve_documents(query)
|
| 401 |
+
|
| 402 |
+
# If no documents found, try rephrasing right away
|
| 403 |
+
if not documents:
|
| 404 |
+
st.write("No relevant documents found, trying with rephrased query...")
|
| 405 |
+
rephrased_query = rephrase_query(query)
|
| 406 |
+
st.write(f"Rephrased query: {rephrased_query}")
|
| 407 |
+
documents, sources = retrieve_documents(rephrased_query)
|
| 408 |
+
|
| 409 |
+
# Format documents into a string for context
|
| 410 |
+
context = "\n\n".join([doc.page_content for doc in documents])
|
| 411 |
+
st.write(f"Retrieved {len(documents)} documents")
|
| 412 |
+
|
| 413 |
+
# If we have context, try the initial RAG approach
|
| 414 |
+
if context:
|
| 415 |
+
st.write("Generating initial answer...")
|
| 416 |
+
initial_answer = generate_answer(context, query)
|
| 417 |
+
|
| 418 |
+
# Evaluate if the initial answer is sufficient
|
| 419 |
+
st.write("Evaluating answer quality...")
|
| 420 |
+
is_sufficient = evaluate_response(query, initial_answer)
|
| 421 |
+
|
| 422 |
+
if is_sufficient:
|
| 423 |
+
# If the initial answer is good, use it
|
| 424 |
+
st.write("Initial answer is sufficient")
|
| 425 |
+
answer = initial_answer
|
| 426 |
+
else:
|
| 427 |
+
# If not sufficient, use the agent with tools
|
| 428 |
+
st.write("Initial answer needs improvement, using advanced tools...")
|
| 429 |
+
agent = setup_agent()
|
| 430 |
+
agent_response = agent.invoke({"input": query})
|
| 431 |
+
answer = agent_response["output"]
|
| 432 |
+
|
| 433 |
+
# Check if the agent used ArXiv and has sources
|
| 434 |
+
if ARXIV_SOURCES:
|
| 435 |
+
st.write(f"Found {len(ARXIV_SOURCES)} ArXiv sources")
|
| 436 |
+
# Replace sources with ArXiv sources
|
| 437 |
+
sources = ARXIV_SOURCES
|
| 438 |
+
else:
|
| 439 |
+
# If no context at all, use the agent directly
|
| 440 |
+
st.write("No relevant documents found, using advanced tools...")
|
| 441 |
+
agent = setup_agent()
|
| 442 |
+
agent_response = agent.invoke({"input": query})
|
| 443 |
+
answer = agent_response["output"]
|
| 444 |
+
|
| 445 |
+
# Check if ArXiv sources are available
|
| 446 |
+
if ARXIV_SOURCES:
|
| 447 |
+
st.write(f"Found {len(ARXIV_SOURCES)} ArXiv sources")
|
| 448 |
+
# Replace sources with ArXiv sources
|
| 449 |
+
sources = ARXIV_SOURCES
|
| 450 |
+
|
| 451 |
+
status.update(label="Completed!", state="complete", expanded=False)
|
| 452 |
+
|
| 453 |
+
# Display answer
|
| 454 |
+
message_placeholder.markdown(answer)
|
| 455 |
+
|
| 456 |
+
# Display sources directly in the message (if available)
|
| 457 |
+
if sources:
|
| 458 |
+
st.markdown("#### Sources")
|
| 459 |
+
for i, source in enumerate(sources):
|
| 460 |
+
title = source.get("title", "Unknown")
|
| 461 |
+
|
| 462 |
+
# Display differently based on source type
|
| 463 |
+
if source.get("type") == "arxiv":
|
| 464 |
+
authors = source.get("authors", "Unknown authors")
|
| 465 |
+
st.markdown(f"**{i+1}. {title}**\nAuthors: {authors}")
|
| 466 |
+
else:
|
| 467 |
+
# PDF source with page number
|
| 468 |
+
page = source.get("page", "Unknown")
|
| 469 |
+
st.markdown(f"**{i+1}. {title}** (Page: {page})")
|
| 470 |
+
|
| 471 |
+
# Add assistant message to chat history with sources
|
| 472 |
+
st.session_state.messages.append({
|
| 473 |
+
"role": "assistant",
|
| 474 |
+
"content": answer,
|
| 475 |
+
"sources": sources if sources else []
|
| 476 |
+
})
|