Spaces:
Build error
Build error
| """ | |
| app.py β Enterprise RAG System β Gradio 5 entry point. | |
| Wires together all pipeline modules and renders the three-panel UI. | |
| Uses gr.State() for per-session document isolation. | |
| """ | |
| import os | |
| import logging | |
| import gradio as gr | |
| from src.ingestion import extract_text_from_pdf, validate_pdf | |
| from src.chunking import chunk_text, chunk_statistics | |
| from src.embeddings import build_faiss_index, embed_texts | |
| from src.retrieval import retrieve_relevant_chunks | |
| from src.generation import generate_answer | |
| from src.evaluation import run_evaluation | |
| from src.observability import trace_rag_query, get_observability_status | |
| from src.metrics import record_query_metrics, get_metrics_summary | |
| from src.utils import format_retrieved_chunks | |
| logging.basicConfig(level=logging.INFO) | |
| logger = logging.getLogger("enterprise-rag.app") | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # PIPELINE FUNCTIONS | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def process_pdf(pdf_file, chunk_size: int, chunk_overlap: int): | |
| """ | |
| Full ingestion pipeline: PDF β text β chunks β embeddings β FAISS index. | |
| Gradio 5 passes uploaded files as a file path string directly. | |
| """ | |
| if pdf_file is None: | |
| return ( | |
| "β οΈ Please upload a PDF file first.", | |
| None, | |
| None, | |
| "", | |
| ) | |
| try: | |
| # Gradio 5: pdf_file is a filepath string | |
| file_path = pdf_file if isinstance(pdf_file, str) else pdf_file.name | |
| with open(file_path, "rb") as f: | |
| file_bytes = f.read() | |
| # Validate size | |
| valid, size_msg = validate_pdf(file_bytes) | |
| if not valid: | |
| return f"β {size_msg}", None, None, "" | |
| # Step 1 β Extract text | |
| extraction = extract_text_from_pdf(file_bytes) | |
| if not extraction["success"]: | |
| return f"β {extraction['error']}", None, None, "" | |
| doc_text = extraction["text"] | |
| page_count = extraction["page_count"] | |
| # Step 2 β Chunk | |
| chunks = chunk_text(doc_text, int(chunk_size), int(chunk_overlap)) | |
| if not chunks: | |
| return "β No chunks created. Document may be too short or empty.", None, None, "" | |
| stats = chunk_statistics(chunks) | |
| chunk_texts = [c["text"] for c in chunks] | |
| # Step 3 β Embed | |
| embeddings = embed_texts(chunk_texts) | |
| # Step 4 β Build FAISS index | |
| faiss_index = build_faiss_index(embeddings) | |
| status = ( | |
| f"β Document processed successfully!\n\n" | |
| f"π Pages: {page_count}\n" | |
| f"π¦ Chunks: {stats['count']}\n" | |
| f"π Avg chunk: {stats['avg_tokens']} tokens\n" | |
| f"π’ Total tokens: {stats['total_tokens']}\n\n" | |
| f"Ready to answer questions." | |
| ) | |
| doc_info = ( | |
| f"**Loaded:** `{os.path.basename(file_path)}` \n" | |
| f"Pages: {page_count} | Chunks: {stats['count']} | " | |
| f"Avg chunk: {stats['avg_tokens']} tokens" | |
| ) | |
| logger.info( | |
| f"PDF ready: {stats['count']} chunks, " | |
| f"{stats['total_tokens']} total tokens" | |
| ) | |
| return status, chunks, faiss_index, doc_info | |
| except Exception as e: | |
| logger.error(f"PDF processing failed: {e}") | |
| return f"β Error: {str(e)}", None, None, "" | |
| def answer_question(query: str, chunks_state, index_state, top_k: int): | |
| """ | |
| Full RAG query pipeline: | |
| embed query β retrieve β generate β evaluate β trace β display. | |
| """ | |
| if not query or not query.strip(): | |
| return "β οΈ Please enter a question.", "", "", "", "" | |
| if chunks_state is None or index_state is None: | |
| return ( | |
| "β οΈ No document loaded. Please upload a PDF first.", | |
| "", "", "", "", | |
| ) | |
| # Normalize chunks to plain text strings | |
| chunk_texts = [ | |
| c["text"] if isinstance(c, dict) else c | |
| for c in chunks_state | |
| ] | |
| try: | |
| # Step 1 β Retrieve | |
| retrieval = retrieve_relevant_chunks( | |
| query=query, | |
| chunks=chunk_texts, | |
| faiss_index=index_state, | |
| top_k=int(top_k), | |
| ) | |
| retrieved_chunks = retrieval["retrieved_chunks"] | |
| scores = retrieval["scores"] | |
| is_relevant = retrieval["is_relevant"] | |
| # Step 2 β Generate | |
| generation = generate_answer( | |
| query=query, | |
| context_chunks=retrieved_chunks, | |
| scores=scores, | |
| is_relevant=is_relevant, | |
| ) | |
| answer = generation["answer"] | |
| prompt_tokens = generation["prompt_tokens"] | |
| response_tokens = generation["response_tokens"] | |
| gen_latency = generation["generation_latency_ms"] | |
| model_used = generation.get("model_used", "unknown") | |
| fallback_used = generation["fallback_used"] | |
| # Step 3 β Evaluate | |
| eval_scores = run_evaluation( | |
| query=query, | |
| answer=answer, | |
| context_chunks=retrieved_chunks, | |
| retrieval_scores=scores, | |
| ) | |
| # Step 4 β Record metrics | |
| record_query_metrics( | |
| retrieval_latency_ms=retrieval["retrieval_latency_ms"], | |
| generation_latency_ms=gen_latency, | |
| prompt_tokens=prompt_tokens, | |
| response_tokens=response_tokens, | |
| eval_scores=eval_scores, | |
| fallback_used=fallback_used, | |
| ) | |
| # Step 5 β Trace | |
| trace_rag_query( | |
| query=query, | |
| answer=answer, | |
| retrieved_chunks=retrieved_chunks, | |
| retrieval_scores=scores, | |
| eval_scores=eval_scores, | |
| retrieval_latency_ms=retrieval["retrieval_latency_ms"], | |
| generation_latency_ms=gen_latency, | |
| prompt_tokens=prompt_tokens, | |
| response_tokens=response_tokens, | |
| model_used=model_used, | |
| fallback_used=fallback_used, | |
| ) | |
| # ββ Format right-panel outputs βββββββββββββββββββββββββββββββββββββ | |
| warning_text = f"\n\nβ οΈ {retrieval['warning']}" if retrieval.get("warning") else "" | |
| chunks_display = format_retrieved_chunks(retrieved_chunks, scores) + warning_text | |
| metrics_display = get_metrics_summary() | |
| eval_display = ( | |
| f"**Answer Quality Scores**\n\n" | |
| f"- Faithfulness: `{eval_scores['faithfulness']:.3f}` β grounded in context\n" | |
| f"- Answer Relevance: `{eval_scores['answer_relevance']:.3f}` β answers the question\n" | |
| f"- Context Precision: `{eval_scores['context_precision']:.3f}` β retrieval quality\n" | |
| f"- **Overall: `{eval_scores['overall']:.3f}`** {eval_scores['quality_label']}\n\n" | |
| f"{eval_scores.get('note', '')}" | |
| ) | |
| obs_display = ( | |
| f"{get_observability_status()}\n\n" | |
| f"**Last trace**\n" | |
| f"- Model: `{model_used}`\n" | |
| f"- Retrieval: `{retrieval['retrieval_latency_ms']:.0f}ms`\n" | |
| f"- Generation: `{gen_latency:.0f}ms`\n" | |
| f"- Total tokens: `{prompt_tokens + response_tokens}`\n" | |
| f"- Fallback used: `{'Yes' if fallback_used else 'No'}`" | |
| ) | |
| return answer, chunks_display, metrics_display, eval_display, obs_display | |
| except Exception as e: | |
| logger.error(f"Query pipeline error: {e}") | |
| return f"β Pipeline error: {str(e)}", "", "", "", "" | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # GRADIO 5 UI | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| with gr.Blocks( | |
| title="Enterprise RAG System", | |
| theme=gr.themes.Soft(primary_hue="blue"), | |
| ) as demo: | |
| # Per-session state β each user gets isolated chunks and FAISS index | |
| chunks_state = gr.State(None) | |
| index_state = gr.State(None) | |
| gr.Markdown( | |
| "# π’ Enterprise Knowledge Retrieval System\n" | |
| "**RAG pipeline Β· Groq LLM Β· FAISS Β· Evaluation Β· Observability**" | |
| ) | |
| with gr.Row(): | |
| # ββ LEFT: Document Upload βββββββββββββββββββββββββββββββββββββββββ | |
| with gr.Column(scale=1, min_width=260): | |
| gr.Markdown("### π Document Upload") | |
| pdf_input = gr.File( | |
| label="Upload PDF", | |
| file_types=[".pdf"], | |
| ) | |
| with gr.Accordion("βοΈ Chunking Settings", open=False): | |
| chunk_size_slider = gr.Slider( | |
| minimum=128, | |
| maximum=1024, | |
| value=512, | |
| step=64, | |
| label="Chunk Size (tokens)", | |
| info="Larger = more context per chunk", | |
| ) | |
| chunk_overlap_slider = gr.Slider( | |
| minimum=0, | |
| maximum=256, | |
| value=64, | |
| step=32, | |
| label="Chunk Overlap (tokens)", | |
| info="Prevents answer loss at boundaries", | |
| ) | |
| top_k_slider = gr.Slider( | |
| minimum=1, | |
| maximum=10, | |
| value=5, | |
| step=1, | |
| label="Top-K Retrieval", | |
| info="Chunks returned per query", | |
| ) | |
| process_btn = gr.Button("π₯ Process Document", variant="primary") | |
| doc_status = gr.Textbox( | |
| label="Status", | |
| lines=6, | |
| interactive=False, | |
| value="No document loaded.", | |
| ) | |
| doc_info_md = gr.Markdown("") | |
| # ββ CENTER: Query & Answer ββββββββββββββββββββββββββββββββββββββββ | |
| with gr.Column(scale=2, min_width=380): | |
| gr.Markdown("### π¬ Ask Questions") | |
| query_input = gr.Textbox( | |
| label="Your Question", | |
| placeholder="Ask anything about the uploaded document...", | |
| lines=3, | |
| ) | |
| ask_btn = gr.Button("π Get Answer", variant="primary", size="lg") | |
| answer_output = gr.Markdown( | |
| value="*Upload a document and ask a question to get started.*" | |
| ) | |
| gr.Markdown( | |
| "---\n" | |
| "**Example questions after uploading:**\n" | |
| "- What are the main topics covered?\n" | |
| "- Summarize the key findings.\n" | |
| "- What risks or challenges are mentioned?\n" | |
| "- What are the specific numbers or statistics?" | |
| ) | |
| # ββ RIGHT: Observability Panel ββββββββββββββββββββββββββββββββββββ | |
| with gr.Column(scale=1, min_width=280): | |
| gr.Markdown("### π Observability") | |
| with gr.Tabs(): | |
| with gr.Tab("π Chunks"): | |
| chunks_output = gr.Markdown( | |
| value="*Retrieved context appears here after a query.*" | |
| ) | |
| with gr.Tab("π Metrics"): | |
| metrics_output = gr.Markdown( | |
| value="*Metrics appear after the first query.*" | |
| ) | |
| with gr.Tab("π§ͺ Evaluation"): | |
| eval_output = gr.Markdown( | |
| value="*Evaluation scores appear after a query.*" | |
| ) | |
| with gr.Tab("π Traces"): | |
| obs_output = gr.Markdown( | |
| value=get_observability_status() | |
| ) | |
| # ββ Event handlers ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| process_btn.click( | |
| fn=process_pdf, | |
| inputs=[pdf_input, chunk_size_slider, chunk_overlap_slider], | |
| outputs=[doc_status, chunks_state, index_state, doc_info_md], | |
| ) | |
| ask_btn.click( | |
| fn=answer_question, | |
| inputs=[query_input, chunks_state, index_state, top_k_slider], | |
| outputs=[answer_output, chunks_output, metrics_output, eval_output, obs_output], | |
| ) | |
| query_input.submit( | |
| fn=answer_question, | |
| inputs=[query_input, chunks_state, index_state, top_k_slider], | |
| outputs=[answer_output, chunks_output, metrics_output, eval_output, obs_output], | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch( | |
| server_name="0.0.0.0", | |
| server_port=7860, | |
| show_error=True, | |
| ) |