towardsinnovationlab commited on
Commit
1736af9
Β·
verified Β·
1 Parent(s): bf3a400

Upload advanced_rag_app.py

Browse files
Files changed (1) hide show
  1. advanced_rag_app.py +471 -0
advanced_rag_app.py ADDED
@@ -0,0 +1,471 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ==============================================================================
2
+ # ADVANCED RAG WITH GPT, LANGCHAIN, AND RAGAS EVALUATION
3
+ # ==============================================================================
4
+ # Enhanced RAG application with quality metrics using RAGAS framework
5
+ # Supports multiple PDF documents
6
+ # ==============================================================================
7
+
8
+ from langchain.retrievers import EnsembleRetriever
9
+ from langchain_community.retrievers import BM25Retriever
10
+ from langchain_community.cross_encoders import HuggingFaceCrossEncoder
11
+ from langchain.retrievers.document_compressors import CrossEncoderReranker
12
+ from sentence_transformers import CrossEncoder
13
+ from langchain.retrievers import ContextualCompressionRetriever
14
+ from langchain_community.document_loaders import PyPDFLoader
15
+ from langchain.text_splitter import RecursiveCharacterTextSplitter
16
+ from langchain_openai import OpenAIEmbeddings, ChatOpenAI
17
+ from langchain_community.vectorstores import FAISS
18
+ from langchain.schema import Document
19
+ from langchain.prompts import PromptTemplate
20
+ from langchain_core.output_parsers import StrOutputParser
21
+ from langchain_core.runnables import RunnablePassthrough
22
+ from datasets import Dataset
23
+ from ragas import evaluate
24
+ from ragas.metrics import (
25
+ faithfulness,
26
+ answer_relevancy,
27
+ context_precision,
28
+ context_recall,
29
+ answer_correctness,
30
+ answer_similarity
31
+ )
32
+ import gradio as gr
33
+ import os
34
+ import pandas as pd
35
+ import json
36
+
37
+ # ==============================================================================
38
+ # GLOBAL VARIABLES
39
+ # ==============================================================================
40
+ rag_chain = None
41
+ current_documents = [] # Changed to list for multiple documents
42
+ openai_api_key = None
43
+ retriever = None
44
+ evaluation_data = []
45
+
46
+ # ==============================================================================
47
+ # HELPER FUNCTIONS
48
+ # ==============================================================================
49
+
50
+ def format_docs(docs):
51
+ """Format retrieved documents with source citations"""
52
+ out = []
53
+ for d in docs:
54
+ src = d.metadata.get("source", "unknown")
55
+ # Extract just the filename from the full path
56
+ src = os.path.basename(src)
57
+ page = d.metadata.get("page", d.metadata.get("page_number", "?"))
58
+
59
+ try:
60
+ page_display = int(page) + 1
61
+ except (ValueError, TypeError):
62
+ page_display = page
63
+
64
+ out.append(f"[{src}:{page_display}] {d.page_content}")
65
+ return "\n\n".join(out)
66
+
67
+
68
+ def validate_api_key(api_key):
69
+ """Validate that API key is provided"""
70
+ if not api_key or not api_key.strip():
71
+ return False
72
+ return True
73
+
74
+
75
+ def process_documents(pdf_files, api_key):
76
+ """Process uploaded PDFs and create RAG chain"""
77
+ global rag_chain, current_documents, openai_api_key, retriever, evaluation_data
78
+
79
+ chatbot_clear = None
80
+ evaluation_data = [] # Reset evaluation data
81
+
82
+ if not validate_api_key(api_key):
83
+ return "⚠️ Please provide a valid OpenAI API key.", chatbot_clear, ""
84
+
85
+ if pdf_files is None or len(pdf_files) == 0:
86
+ return "⚠️ Please upload at least one PDF file.", chatbot_clear, ""
87
+
88
+ try:
89
+ openai_api_key = api_key.strip()
90
+ os.environ["OPENAI_API_KEY"] = openai_api_key
91
+
92
+ # Process all uploaded PDFs
93
+ all_docs = []
94
+ current_documents = []
95
+ total_pages = 0
96
+
97
+ for pdf_file in pdf_files:
98
+ loader = PyPDFLoader(pdf_file.name)
99
+ docs = loader.load()
100
+ all_docs.extend(docs)
101
+ current_documents.append(os.path.basename(pdf_file.name))
102
+ total_pages += len(docs)
103
+
104
+ # Split all documents
105
+ splitter = RecursiveCharacterTextSplitter(
106
+ separators=["\n\n", "\n", ". ", " ", ""],
107
+ chunk_size=1000,
108
+ chunk_overlap=100
109
+ )
110
+ chunked_docs = splitter.split_documents(all_docs)
111
+
112
+ # Create embeddings and vector store
113
+ embeddings = OpenAIEmbeddings(
114
+ model="text-embedding-3-small",
115
+ openai_api_key=openai_api_key
116
+ )
117
+
118
+ db = FAISS.from_documents(chunked_docs, embeddings)
119
+
120
+ retriever_1 = db.as_retriever(search_type="similarity",search_kwargs={'k': 10})
121
+
122
+ retriever_2 = BM25Retriever.from_documents(chunked_docs, search_kwargs={"k": 10})
123
+
124
+ ensemble_retriever = EnsembleRetriever(retrievers=[retriever_1, retriever_2], weights=[0.7, 0.3])
125
+
126
+ cross_encoder_model = HuggingFaceCrossEncoder(model_name="cross-encoder/ms-marco-MiniLM-L-12-v2")
127
+
128
+ reranker = CrossEncoderReranker(model=cross_encoder_model,top_n=10)
129
+
130
+ reranking_retriever = ContextualCompressionRetriever(base_compressor=reranker,base_retriever=ensemble_retriever)
131
+
132
+ retriever=reranking_retriever
133
+
134
+ # Create LLM and prompt
135
+ llm = ChatOpenAI(
136
+ model="gpt-5-mini",
137
+ temperature=0.2,
138
+ openai_api_key=openai_api_key
139
+ )
140
+
141
+ prompt_template = """You are a professional research scientist involved in document data analysis.
142
+ Use the following context to answer the question using information provided by the documents.
143
+ Answer using ONLY these passages. Cite sources as [filename:page] after each claim.
144
+ Provide an answer in bullet points.
145
+ If you can't find it, say you don't know.
146
+
147
+ Question:
148
+ {question}
149
+
150
+ Passages:
151
+ {context}
152
+
153
+ Answer:"""
154
+
155
+ prompt = PromptTemplate(
156
+ input_variables=["context", "question"],
157
+ template=prompt_template,
158
+ )
159
+
160
+ llm_chain = prompt | llm | StrOutputParser()
161
+
162
+ rag_chain = (
163
+ {"context": reranking_retriever | format_docs, "question": RunnablePassthrough()}
164
+ | llm_chain
165
+ )
166
+
167
+ # Create status message with document list
168
+ doc_list = "\n".join([f" β€’ {doc}" for doc in current_documents])
169
+ status_msg = (
170
+ f"βœ… Documents processed successfully!\n\n"
171
+ f"πŸ“„ **Documents loaded ({len(current_documents)}):**\n{doc_list}\n\n"
172
+ f"πŸ“Š Total pages: {total_pages}\n"
173
+ f"πŸ“¦ Chunks created: {len(chunked_docs)}\n\n"
174
+ f"You can now ask questions and evaluate responses!"
175
+ )
176
+
177
+ return status_msg, chatbot_clear, ""
178
+
179
+ except Exception as e:
180
+ return f"❌ Error processing documents: {str(e)}", chatbot_clear, ""
181
+
182
+
183
+ def chat_with_document(message, history):
184
+ """Handle chat interactions with the documents"""
185
+ global rag_chain, current_documents, retriever, evaluation_data
186
+
187
+ history.append({"role": "user", "content": message})
188
+
189
+ if rag_chain is None:
190
+ history.append({
191
+ "role": "assistant",
192
+ "content": "⚠️ Please upload and process PDF documents first."
193
+ })
194
+ return history
195
+
196
+ if not message.strip():
197
+ history.append({
198
+ "role": "assistant",
199
+ "content": "⚠️ Please enter a question."
200
+ })
201
+ return history
202
+
203
+ try:
204
+ # Retrieve contexts for RAGAS evaluation
205
+ retrieved_docs = retriever.invoke(message)
206
+ contexts = [doc.page_content for doc in retrieved_docs]
207
+
208
+ # Get response from RAG chain
209
+ response = rag_chain.invoke(message)
210
+
211
+ if isinstance(response, dict):
212
+ res_text = response.get("answer", response.get("result", str(response)))
213
+ else:
214
+ res_text = str(response)
215
+
216
+ # Store data for RAGAS evaluation
217
+ evaluation_data.append({
218
+ "question": message,
219
+ "answer": res_text,
220
+ "contexts": contexts
221
+ })
222
+
223
+ history.append({"role": "assistant", "content": res_text})
224
+ return history
225
+
226
+ except Exception as e:
227
+ error_msg = f"❌ Error generating response: {str(e)}"
228
+ history.append({"role": "assistant", "content": error_msg})
229
+ return history
230
+
231
+
232
+ def evaluate_rag_performance():
233
+ """Evaluate RAG performance using RAGAS metrics"""
234
+ global evaluation_data, openai_api_key
235
+
236
+ if not evaluation_data:
237
+ return "⚠️ No evaluation data available. Please ask some questions first."
238
+
239
+ try:
240
+ # Prepare dataset for RAGAS
241
+ dataset_dict = {
242
+ "question": [item["question"] for item in evaluation_data],
243
+ "answer": [item["answer"] for item in evaluation_data],
244
+ "contexts": [item["contexts"] for item in evaluation_data],
245
+ }
246
+
247
+ dataset = Dataset.from_dict(dataset_dict)
248
+
249
+ # Run RAGAS evaluation
250
+ # Using only metrics that don't require ground truth (reference answers)
251
+ result = evaluate(
252
+ dataset,
253
+ metrics=[
254
+ faithfulness,
255
+ answer_relevancy,
256
+ ],
257
+ llm=ChatOpenAI(model="gpt-4o-mini", openai_api_key=openai_api_key),
258
+ embeddings=OpenAIEmbeddings(openai_api_key=openai_api_key),
259
+ )
260
+
261
+ # Convert to DataFrame for better display
262
+ df = result.to_pandas()
263
+
264
+ # Calculate average scores from the result directly
265
+ metrics_summary = "## πŸ“Š RAGAS Evaluation Results\n\n"
266
+ metrics_summary += "### Average Scores:\n"
267
+
268
+ # Get metric scores safely
269
+ metric_cols = ['faithfulness', 'answer_relevancy']
270
+ metric_scores = {}
271
+
272
+ for col in metric_cols:
273
+ if col in df.columns:
274
+ # Convert to numeric, handling any non-numeric values
275
+ numeric_values = pd.to_numeric(df[col], errors='coerce')
276
+ avg_score = numeric_values.mean()
277
+ if not pd.isna(avg_score):
278
+ metric_scores[col] = avg_score
279
+ metrics_summary += f"- **{col.replace('_', ' ').title()}**: {avg_score:.4f}\n"
280
+
281
+ metrics_summary += "\n### Metric Explanations:\n"
282
+ metrics_summary += "- **Faithfulness** (0-1): Measures if the answer is factually consistent with the retrieved context. Higher scores mean the answer doesn't hallucinate or contradict the source.\n"
283
+ metrics_summary += "- **Answer Relevancy** (0-1): Measures how relevant the answer is to the question asked. Higher scores mean better alignment with the user's query.\n"
284
+
285
+
286
+ metrics_summary += "\n### Interpretation Guide:\n"
287
+ metrics_summary += "- **0.9 - 1.0**: Excellent performance\n"
288
+ metrics_summary += "- **0.7 - 0.9**: Good performance\n"
289
+ metrics_summary += "- **0.5 - 0.7**: Moderate performance (needs improvement)\n"
290
+ metrics_summary += "- **< 0.5**: Poor performance (requires significant optimization)\n"
291
+
292
+ metrics_summary += f"\n### Total Questions Evaluated: {len(evaluation_data)}\n"
293
+
294
+ # Add document info
295
+ if current_documents:
296
+ metrics_summary += f"\n### Documents in Index: {len(current_documents)}\n"
297
+
298
+ return metrics_summary
299
+
300
+ except Exception as e:
301
+ return f"❌ Error during evaluation: {str(e)}"
302
+
303
+
304
+ def export_evaluation_data():
305
+ """Export evaluation data as JSON"""
306
+ global evaluation_data, current_documents
307
+
308
+ if not evaluation_data:
309
+ return None
310
+
311
+ try:
312
+ # Create a temporary file with metadata
313
+ output_data = {
314
+ "documents": current_documents,
315
+ "evaluation_data": evaluation_data,
316
+ "total_questions": len(evaluation_data)
317
+ }
318
+
319
+ output_path = "ragas_evaluation_data.json"
320
+ with open(output_path, 'w') as f:
321
+ json.dump(output_data, f, indent=2)
322
+ return output_path
323
+ except Exception as e:
324
+ print(f"Error exporting data: {str(e)}")
325
+ return None
326
+
327
+
328
+ def clear_chat():
329
+ """Clear the chat history and evaluation data"""
330
+ global evaluation_data
331
+ evaluation_data = [] # Reset evaluation data when clearing chat
332
+ return [], "" # Return empty chatbot and empty eval_summary
333
+
334
+
335
+
336
+ # ==============================================================================
337
+ # GRADIO INTERFACE
338
+ # ==============================================================================
339
+
340
+ with gr.Blocks(title="RAG with RAGAS Evaluation", theme=gr.themes.Soft()) as demo:
341
+
342
+ gr.Markdown(
343
+ """
344
+ # πŸ“š Multi-Document Q&A Analysis
345
+ ### Advanced RAG System Powered by OpenAI GPT models, LangChain & RAGAS
346
+
347
+ Upload multiple PDFs, ask questions across all documents, and evaluate your RAG system's performance with industry-standard metrics.
348
+ """
349
+ )
350
+
351
+ with gr.Row():
352
+ with gr.Column(scale=1):
353
+ gr.Markdown(
354
+ """
355
+ ### πŸ“‹ How to Use
356
+ 1. Enter your OpenAI API key
357
+ 2. Upload one or more PDF documents
358
+ 3. Process the documents
359
+ 4. Ask questions in the chat
360
+ 5. Click "Evaluate" to see performance metrics
361
+
362
+ ---
363
+
364
+ πŸ’‘ **RAGAS Metrics**:
365
+ - Faithfulness: Factual accuracy
366
+ - Answer Relevancy: Question alignment
367
+
368
+ πŸ“ **Multi-Document Support**:
369
+ - Upload multiple PDFs at once
370
+ - Search across all documents
371
+ - Get citations with document names
372
+ """
373
+ )
374
+
375
+ gr.Markdown("### πŸ”‘ API Configuration")
376
+ api_key_input = gr.Textbox(
377
+ label="OpenAI API Key",
378
+ type="password",
379
+ placeholder="sk-...",
380
+ info="Required for GPT models and RAGAS evaluation"
381
+ )
382
+
383
+ gr.Markdown("### πŸ“€ Upload Documents")
384
+ pdf_input = gr.File(
385
+ label="Upload PDF Documents",
386
+ file_types=[".pdf"],
387
+ type="filepath",
388
+ file_count="multiple" # Enable multiple file upload
389
+ )
390
+ process_btn = gr.Button("πŸ“„ Process Documents", variant="primary", size="lg")
391
+
392
+ status_output = gr.Textbox(
393
+ label="Status",
394
+ lines=8, # Increased to show multiple documents
395
+ interactive=False,
396
+ placeholder="Enter API key, upload PDFs, and click 'Process Documents'..."
397
+ )
398
+
399
+ gr.Markdown("### πŸ“ˆ Evaluation")
400
+ evaluate_btn = gr.Button("πŸ” Evaluate RAG Performance", variant="secondary", size="lg")
401
+ export_btn = gr.Button("πŸ’Ύ Export Evaluation Data", size="sm")
402
+ export_file = gr.File(label="Download Evaluation Data", visible=True)
403
+
404
+ with gr.Column(scale=2):
405
+ gr.Markdown("### πŸ’¬ Chat with Your Documents")
406
+ chatbot = gr.Chatbot(
407
+ height=400,
408
+ placeholder="Upload and process documents to start...",
409
+ show_label=False,
410
+ type="messages"
411
+ )
412
+
413
+ msg = gr.Textbox(
414
+ label="Enter your question",
415
+ placeholder="Type your question here (searches across all uploaded documents)...",
416
+ lines=2
417
+ )
418
+
419
+ with gr.Row():
420
+ submit_btn = gr.Button("πŸ“€ Send", variant="primary", scale=4)
421
+ clear_btn = gr.Button("πŸ—‘οΈ Clear Chat", scale=1)
422
+
423
+ gr.Markdown("### πŸ“Š Evaluation Results")
424
+ eval_summary = gr.Markdown(value="")
425
+
426
+ # Event handlers
427
+ process_btn.click(
428
+ fn=process_documents, # Changed function name
429
+ inputs=[pdf_input, api_key_input],
430
+ outputs=[status_output, chatbot, eval_summary]
431
+ )
432
+
433
+ submit_btn.click(
434
+ fn=chat_with_document,
435
+ inputs=[msg, chatbot],
436
+ outputs=[chatbot]
437
+ ).then(
438
+ lambda: "",
439
+ outputs=[msg]
440
+ )
441
+
442
+ msg.submit(
443
+ fn=chat_with_document,
444
+ inputs=[msg, chatbot],
445
+ outputs=[chatbot]
446
+ ).then(
447
+ lambda: "",
448
+ outputs=[msg]
449
+ )
450
+
451
+ clear_btn.click(
452
+ fn=clear_chat,
453
+ outputs=[chatbot, eval_summary]
454
+ )
455
+
456
+ evaluate_btn.click(
457
+ fn=evaluate_rag_performance,
458
+ outputs=[eval_summary]
459
+ )
460
+
461
+ export_btn.click(
462
+ fn=export_evaluation_data,
463
+ outputs=[export_file]
464
+ )
465
+
466
+ # ==============================================================================
467
+ # LAUNCH APPLICATION
468
+ # ==============================================================================
469
+
470
+ if __name__ == "__main__":
471
+ demo.launch(share=False, debug=True)