Upload 6 files
Browse filesinitiated the project
- .gitattributes +1 -0
- README.md +85 -7
- app.py +56 -0
- clinical_guidelines.pdf +3 -0
- rag_pipeline.py +78 -0
- requirements.txt +9 -0
- utils.py +72 -0
.gitattributes
CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
36 |
+
clinical_guidelines.pdf filter=lfs diff=lfs merge=lfs -text
|
README.md
CHANGED
@@ -1,13 +1,91 @@
|
|
1 |
---
|
2 |
-
title:
|
3 |
-
emoji:
|
4 |
-
colorFrom:
|
5 |
-
colorTo:
|
6 |
sdk: gradio
|
7 |
-
sdk_version: 5.
|
8 |
app_file: app.py
|
9 |
pinned: false
|
10 |
-
|
11 |
---
|
12 |
|
13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
---
|
2 |
+
title: Healthcare Guidelines RAG Chatbot
|
3 |
+
emoji: 📄
|
4 |
+
colorFrom: blue
|
5 |
+
colorTo: indigo
|
6 |
sdk: gradio
|
7 |
+
sdk_version: 5.29.0
|
8 |
app_file: app.py
|
9 |
pinned: false
|
10 |
+
license: apache-2.0
|
11 |
---
|
12 |
|
13 |
+
|
14 |
+
# Healthcare Guidelines RAG Chatbot
|
15 |
+
|
16 |
+
This application provides a Retrieval-Augmented Generation (RAG) system for healthcare clinical guidelines. It uses FAISS for efficient similarity search and a Hugging Face model for generating responses.
|
17 |
+
|
18 |
+
## Features
|
19 |
+
|
20 |
+
- PDF document processing and chunking
|
21 |
+
- FAISS-based semantic search
|
22 |
+
- Response generation using FLAN-T5
|
23 |
+
- Gradio web interface
|
24 |
+
- Strict grounding in provided guidelines
|
25 |
+
- Safety measures and disclaimers
|
26 |
+
|
27 |
+
## Setup
|
28 |
+
|
29 |
+
### Option 1: Local Setup
|
30 |
+
2. Install dependencies:
|
31 |
+
```bash
|
32 |
+
pip install -r requirements.txt
|
33 |
+
```
|
34 |
+
|
35 |
+
3. Place your clinical guidelines PDF in the directory:
|
36 |
+
```
|
37 |
+
# Copy your PDF to clinical_guidelines.pdf
|
38 |
+
```
|
39 |
+
|
40 |
+
1. Build the Docker image:
|
41 |
+
```bash
|
42 |
+
docker build -t healthcare-rag-chatbot .
|
43 |
+
```
|
44 |
+
|
45 |
+
2. Run the container:
|
46 |
+
```bash
|
47 |
+
docker run -p 7860:7860 -v $(pwd)/data:/app/data healthcare-rag-chatbot
|
48 |
+
```
|
49 |
+
|
50 |
+
The `-v` flag mounts your local `data` directory to the container, allowing you to easily update the clinical guidelines PDF without rebuilding the image.
|
51 |
+
|
52 |
+
## Usage
|
53 |
+
|
54 |
+
1. Run the application:
|
55 |
+
```bash
|
56 |
+
# If using local setup:
|
57 |
+
python app.py
|
58 |
+
|
59 |
+
# If using Docker:
|
60 |
+
# The application will start automatically when running the container
|
61 |
+
```
|
62 |
+
|
63 |
+
2. Open your browser and navigate to the provided local URL (typically http://127.0.0.1:7860)
|
64 |
+
|
65 |
+
3. Enter your question about clinical guidelines in the input box
|
66 |
+
|
67 |
+
## Important Notes
|
68 |
+
|
69 |
+
- The system will only provide information that is explicitly stated in the provided guidelines
|
70 |
+
- All responses include a medical disclaimer
|
71 |
+
- The system will respond with "This information is not available in the current guidelines" when uncertain
|
72 |
+
|
73 |
+
## Deployment
|
74 |
+
|
75 |
+
To deploy on Hugging Face Spaces:
|
76 |
+
|
77 |
+
1. Create a new Space on Hugging Face
|
78 |
+
2. Connect your GitHub repository
|
79 |
+
3. Select Gradio as the SDK
|
80 |
+
4. The app will automatically deploy
|
81 |
+
|
82 |
+
## Safety Measures
|
83 |
+
|
84 |
+
- All responses are strictly grounded in the provided guidelines
|
85 |
+
- A medical disclaimer is included with every response
|
86 |
+
- The system explicitly states when information is not available
|
87 |
+
- No medical advice is provided without proper context
|
88 |
+
|
89 |
+
## License
|
90 |
+
|
91 |
+
This project is for educational purposes only. Always consult healthcare professionals for medical advice.
|
app.py
ADDED
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import gradio as gr
|
2 |
+
from rag_pipeline import HealthcareRAG
|
3 |
+
import os
|
4 |
+
|
5 |
+
# Initialize RAG system
|
6 |
+
rag = HealthcareRAG()
|
7 |
+
|
8 |
+
def process_query(query: str) -> tuple:
|
9 |
+
"""Process user query and return response with retrieved chunks."""
|
10 |
+
result = rag.query(query)
|
11 |
+
|
12 |
+
# Format retrieved chunks for display
|
13 |
+
chunks_text = "\n\n---\n\n".join(result["retrieved_chunks"])
|
14 |
+
|
15 |
+
return result["response"], chunks_text
|
16 |
+
|
17 |
+
# Create Gradio interface
|
18 |
+
with gr.Blocks(title="Healthcare Guidelines Assistant") as demo:
|
19 |
+
gr.Markdown("""
|
20 |
+
# Healthcare Guidelines Assistant
|
21 |
+
|
22 |
+
This assistant provides information based on clinical guidelines.
|
23 |
+
Please note that this is for educational purposes only and not medical advice.
|
24 |
+
|
25 |
+
**DISCLAIMER**: This information is for educational purposes only and not medical advice.
|
26 |
+
Always consult with healthcare professionals for medical decisions.
|
27 |
+
""")
|
28 |
+
|
29 |
+
with gr.Row():
|
30 |
+
with gr.Column():
|
31 |
+
query_input = gr.Textbox(
|
32 |
+
label="Your Question",
|
33 |
+
placeholder="Enter your question about clinical guidelines...",
|
34 |
+
lines=3
|
35 |
+
)
|
36 |
+
submit_btn = gr.Button("Submit")
|
37 |
+
|
38 |
+
with gr.Row():
|
39 |
+
with gr.Column():
|
40 |
+
response_output = gr.Textbox(
|
41 |
+
label="Response",
|
42 |
+
lines=5
|
43 |
+
)
|
44 |
+
chunks_output = gr.Textbox(
|
45 |
+
label="Retrieved Guidelines",
|
46 |
+
lines=10
|
47 |
+
)
|
48 |
+
|
49 |
+
submit_btn.click(
|
50 |
+
fn=process_query,
|
51 |
+
inputs=query_input,
|
52 |
+
outputs=[response_output, chunks_output]
|
53 |
+
)
|
54 |
+
|
55 |
+
if __name__ == "__main__":
|
56 |
+
demo.launch()
|
clinical_guidelines.pdf
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:74e988ffe0603263ce131d043a187302d5845fbc4ff30aa74f9190ce99ff5016
|
3 |
+
size 4917213
|
rag_pipeline.py
ADDED
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from transformers import AutoModelForSeq2SeqLM, AutoTokenizer
|
2 |
+
import torch
|
3 |
+
from utils import DocumentProcessor
|
4 |
+
import os
|
5 |
+
from typing import List, Dict
|
6 |
+
|
7 |
+
class HealthcareRAG:
|
8 |
+
def __init__(self,
|
9 |
+
model_name: str = "google/flan-t5-base",
|
10 |
+
index_path: str = "faiss_index.bin",
|
11 |
+
pdf_path: str = "clinical_guidelines.pdf"):
|
12 |
+
|
13 |
+
self.device = "cuda" if torch.cuda.is_available() else "cpu"
|
14 |
+
self.tokenizer = AutoTokenizer.from_pretrained(model_name)
|
15 |
+
self.model = AutoModelForSeq2SeqLM.from_pretrained(model_name).to(self.device)
|
16 |
+
|
17 |
+
self.doc_processor = DocumentProcessor()
|
18 |
+
|
19 |
+
# Initialize or load FAISS index
|
20 |
+
if os.path.exists(index_path):
|
21 |
+
self.index = self.doc_processor.load_index(index_path)
|
22 |
+
# Load chunks from a saved file (you might want to save/load chunks separately)
|
23 |
+
self.chunks = [] # Load your chunks here
|
24 |
+
else:
|
25 |
+
self.chunks, self.index = self.doc_processor.process_document(pdf_path, index_path)
|
26 |
+
|
27 |
+
def generate_response(self, query: str, retrieved_chunks: List[str]) -> str:
|
28 |
+
"""Generate response using the LLM."""
|
29 |
+
if not retrieved_chunks:
|
30 |
+
return "This information is not available in the current guidelines."
|
31 |
+
|
32 |
+
# Prepare context and prompt
|
33 |
+
context = "\n".join(retrieved_chunks)
|
34 |
+
prompt = f"""Based on the following clinical guidelines, answer the question.
|
35 |
+
If the information is not explicitly stated in the guidelines, respond with "This information is not available in the current guidelines."
|
36 |
+
|
37 |
+
Guidelines:
|
38 |
+
{context}
|
39 |
+
|
40 |
+
Question: {query}
|
41 |
+
|
42 |
+
Answer:"""
|
43 |
+
|
44 |
+
# Generate response
|
45 |
+
inputs = self.tokenizer(prompt, return_tensors="pt", max_length=1024, truncation=True).to(self.device)
|
46 |
+
outputs = self.model.generate(
|
47 |
+
**inputs,
|
48 |
+
max_length=200,
|
49 |
+
num_beams=4,
|
50 |
+
temperature=0.7,
|
51 |
+
top_p=0.9,
|
52 |
+
do_sample=True
|
53 |
+
)
|
54 |
+
|
55 |
+
response = self.tokenizer.decode(outputs[0], skip_special_tokens=True)
|
56 |
+
|
57 |
+
# Add disclaimer
|
58 |
+
if response and response != "This information is not available in the current guidelines.":
|
59 |
+
response += "\n\nDISCLAIMER: This information is for educational purposes only and not medical advice."
|
60 |
+
|
61 |
+
return response
|
62 |
+
|
63 |
+
def query(self, user_query: str) -> Dict[str, str]:
|
64 |
+
"""Process user query and return response with retrieved chunks."""
|
65 |
+
# Retrieve relevant chunks
|
66 |
+
retrieved_chunks = self.doc_processor.retrieve_chunks(
|
67 |
+
user_query,
|
68 |
+
self.index,
|
69 |
+
self.chunks
|
70 |
+
)
|
71 |
+
|
72 |
+
# Generate response
|
73 |
+
response = self.generate_response(user_query, retrieved_chunks)
|
74 |
+
|
75 |
+
return {
|
76 |
+
"response": response,
|
77 |
+
"retrieved_chunks": retrieved_chunks
|
78 |
+
}
|
requirements.txt
ADDED
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
gradio==4.19.2
|
2 |
+
sentence-transformers==2.5.1
|
3 |
+
faiss-cpu==1.7.4
|
4 |
+
PyMuPDF==1.23.26
|
5 |
+
transformers==4.38.2
|
6 |
+
torch==2.2.1
|
7 |
+
numpy==1.26.4
|
8 |
+
pandas==2.2.1
|
9 |
+
python-dotenv==1.0.1
|
utils.py
ADDED
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import fitz # PyMuPDF
|
2 |
+
import numpy as np
|
3 |
+
from sentence_transformers import SentenceTransformer
|
4 |
+
import faiss
|
5 |
+
import os
|
6 |
+
from typing import List, Tuple
|
7 |
+
|
8 |
+
class DocumentProcessor:
|
9 |
+
def __init__(self, model_name: str = "all-MiniLM-L6-v2"):
|
10 |
+
self.model = SentenceTransformer(model_name)
|
11 |
+
self.chunk_size = 500 # tokens
|
12 |
+
self.chunk_overlap = 50 # tokens
|
13 |
+
|
14 |
+
def extract_text_from_pdf(self, pdf_path: str) -> str:
|
15 |
+
"""Extract text from PDF file."""
|
16 |
+
doc = fitz.open(pdf_path)
|
17 |
+
text = ""
|
18 |
+
for page in doc:
|
19 |
+
text += page.get_text()
|
20 |
+
return text
|
21 |
+
|
22 |
+
def chunk_text(self, text: str) -> List[str]:
|
23 |
+
"""Split text into overlapping chunks."""
|
24 |
+
words = text.split()
|
25 |
+
chunks = []
|
26 |
+
|
27 |
+
for i in range(0, len(words), self.chunk_size - self.chunk_overlap):
|
28 |
+
chunk = " ".join(words[i:i + self.chunk_size])
|
29 |
+
chunks.append(chunk)
|
30 |
+
|
31 |
+
return chunks
|
32 |
+
|
33 |
+
def create_embeddings(self, chunks: List[str]) -> np.ndarray:
|
34 |
+
"""Create embeddings for text chunks."""
|
35 |
+
return self.model.encode(chunks)
|
36 |
+
|
37 |
+
def build_faiss_index(self, embeddings: np.ndarray) -> faiss.Index:
|
38 |
+
"""Build and return a FAISS index."""
|
39 |
+
dimension = embeddings.shape[1]
|
40 |
+
index = faiss.IndexFlatL2(dimension)
|
41 |
+
index.add(embeddings.astype('float32'))
|
42 |
+
return index
|
43 |
+
|
44 |
+
def save_index(self, index: faiss.Index, path: str):
|
45 |
+
"""Save FAISS index to disk."""
|
46 |
+
faiss.write_index(index, path)
|
47 |
+
|
48 |
+
def load_index(self, path: str) -> faiss.Index:
|
49 |
+
"""Load FAISS index from disk."""
|
50 |
+
return faiss.read_index(path)
|
51 |
+
|
52 |
+
def process_document(self, pdf_path: str, index_path: str) -> Tuple[List[str], faiss.Index]:
|
53 |
+
"""Process document and create FAISS index."""
|
54 |
+
# Extract and chunk text
|
55 |
+
text = self.extract_text_from_pdf(pdf_path)
|
56 |
+
chunks = self.chunk_text(text)
|
57 |
+
|
58 |
+
# Create embeddings and index
|
59 |
+
embeddings = self.create_embeddings(chunks)
|
60 |
+
index = self.build_faiss_index(embeddings)
|
61 |
+
|
62 |
+
# Save index
|
63 |
+
self.save_index(index, index_path)
|
64 |
+
|
65 |
+
return chunks, index
|
66 |
+
|
67 |
+
def retrieve_chunks(self, query: str, index: faiss.Index, chunks: List[str], k: int = 5) -> List[str]:
|
68 |
+
"""Retrieve most relevant chunks for a query."""
|
69 |
+
query_embedding = self.model.encode([query])
|
70 |
+
distances, indices = index.search(query_embedding.astype('float32'), k)
|
71 |
+
|
72 |
+
return [chunks[i] for i in indices[0]]
|