Faraz618's picture
Update app.py
8d4686d verified
Raw
History Blame Contribute Delete
13.4 kB
"""
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,
)