Spaces:
Build error
Build error
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,12 +1,8 @@
|
|
| 1 |
"""
|
| 2 |
-
app.py β Enterprise RAG System β Gradio
|
| 3 |
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
Architecture: stateful single-user session using Gradio's gr.State()
|
| 8 |
-
In production multi-user deployment, each session would have isolated
|
| 9 |
-
vector store state (Redis or per-session FAISS index in memory).
|
| 10 |
"""
|
| 11 |
|
| 12 |
import os
|
|
@@ -21,138 +17,129 @@ from src.generation import generate_answer
|
|
| 21 |
from src.evaluation import run_evaluation
|
| 22 |
from src.observability import trace_rag_query, get_observability_status
|
| 23 |
from src.metrics import record_query_metrics, get_metrics_summary
|
| 24 |
-
from src.utils import format_retrieved_chunks
|
| 25 |
|
| 26 |
logging.basicConfig(level=logging.INFO)
|
| 27 |
logger = logging.getLogger("enterprise-rag.app")
|
| 28 |
|
|
|
|
| 29 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 30 |
-
#
|
| 31 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 32 |
|
| 33 |
def process_pdf(pdf_file, chunk_size: int, chunk_overlap: int):
|
| 34 |
"""
|
| 35 |
Full ingestion pipeline: PDF β text β chunks β embeddings β FAISS index.
|
| 36 |
|
| 37 |
-
|
| 38 |
-
status_message: shown in the UI
|
| 39 |
-
chunks_state: stored in gr.State for use during queries
|
| 40 |
-
index_state: FAISS index stored in gr.State
|
| 41 |
"""
|
| 42 |
if pdf_file is None:
|
| 43 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
|
| 45 |
try:
|
| 46 |
-
#
|
| 47 |
-
|
|
|
|
|
|
|
| 48 |
file_bytes = f.read()
|
| 49 |
|
| 50 |
-
#
|
| 51 |
valid, size_msg = validate_pdf(file_bytes)
|
| 52 |
if not valid:
|
| 53 |
-
return f"β {size_msg}", None, None, "
|
| 54 |
|
| 55 |
-
# Step 1
|
| 56 |
extraction = extract_text_from_pdf(file_bytes)
|
| 57 |
if not extraction["success"]:
|
| 58 |
-
return f"β {extraction['error']}", None, None, "
|
| 59 |
|
| 60 |
doc_text = extraction["text"]
|
| 61 |
page_count = extraction["page_count"]
|
| 62 |
|
| 63 |
-
# Step 2
|
| 64 |
chunks = chunk_text(doc_text, int(chunk_size), int(chunk_overlap))
|
| 65 |
if not chunks:
|
| 66 |
-
return "β No chunks created. Document may be too short.", None, None, ""
|
| 67 |
|
| 68 |
stats = chunk_statistics(chunks)
|
| 69 |
chunk_texts = [c["text"] for c in chunks]
|
| 70 |
|
| 71 |
-
# Step 3
|
| 72 |
embeddings = embed_texts(chunk_texts)
|
| 73 |
|
| 74 |
-
# Step 4
|
| 75 |
faiss_index = build_faiss_index(embeddings)
|
| 76 |
|
| 77 |
status = (
|
| 78 |
f"β
Document processed successfully!\n\n"
|
| 79 |
f"π Pages: {page_count}\n"
|
| 80 |
f"π¦ Chunks: {stats['count']}\n"
|
| 81 |
-
f"π Avg chunk
|
| 82 |
f"π’ Total tokens: {stats['total_tokens']}\n\n"
|
| 83 |
f"Ready to answer questions."
|
| 84 |
)
|
| 85 |
|
| 86 |
doc_info = (
|
| 87 |
-
f"**
|
| 88 |
f"Pages: {page_count} | Chunks: {stats['count']} | "
|
| 89 |
f"Avg chunk: {stats['avg_tokens']} tokens"
|
| 90 |
)
|
| 91 |
|
| 92 |
-
logger.info(
|
|
|
|
|
|
|
|
|
|
| 93 |
return status, chunks, faiss_index, doc_info
|
| 94 |
|
| 95 |
except Exception as e:
|
| 96 |
logger.error(f"PDF processing failed: {e}")
|
| 97 |
-
return f"β
|
| 98 |
|
| 99 |
|
| 100 |
-
|
| 101 |
-
# QUERY PIPELINE
|
| 102 |
-
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 103 |
-
|
| 104 |
-
def answer_question(
|
| 105 |
-
query: str,
|
| 106 |
-
chunks_state,
|
| 107 |
-
index_state,
|
| 108 |
-
top_k: int,
|
| 109 |
-
):
|
| 110 |
"""
|
| 111 |
-
Full RAG query pipeline:
|
| 112 |
-
|
| 113 |
-
Returns:
|
| 114 |
-
answer_text: shown in the center panel
|
| 115 |
-
chunks_display: retrieved chunks for the right panel
|
| 116 |
-
metrics_display: latency/token metrics for the right panel
|
| 117 |
-
eval_display: evaluation scores for the right panel
|
| 118 |
-
obs_display: observability status
|
| 119 |
"""
|
| 120 |
-
empty_right = ("", "", "", "")
|
| 121 |
-
|
| 122 |
if not query or not query.strip():
|
| 123 |
-
return "β οΈ Please enter a question.",
|
| 124 |
|
| 125 |
if chunks_state is None or index_state is None:
|
| 126 |
return (
|
| 127 |
"β οΈ No document loaded. Please upload a PDF first.",
|
| 128 |
-
|
| 129 |
)
|
| 130 |
|
|
|
|
| 131 |
chunk_texts = [
|
| 132 |
-
c["text"] if isinstance(c, dict) else c
|
|
|
|
| 133 |
]
|
| 134 |
|
| 135 |
try:
|
| 136 |
-
# Step 1
|
| 137 |
retrieval = retrieve_relevant_chunks(
|
| 138 |
query=query,
|
| 139 |
chunks=chunk_texts,
|
| 140 |
faiss_index=index_state,
|
| 141 |
top_k=int(top_k),
|
| 142 |
)
|
| 143 |
-
|
| 144 |
retrieved_chunks = retrieval["retrieved_chunks"]
|
| 145 |
scores = retrieval["scores"]
|
| 146 |
is_relevant = retrieval["is_relevant"]
|
| 147 |
|
| 148 |
-
# Step 2
|
| 149 |
generation = generate_answer(
|
| 150 |
query=query,
|
| 151 |
context_chunks=retrieved_chunks,
|
| 152 |
scores=scores,
|
| 153 |
is_relevant=is_relevant,
|
| 154 |
)
|
| 155 |
-
|
| 156 |
answer = generation["answer"]
|
| 157 |
prompt_tokens = generation["prompt_tokens"]
|
| 158 |
response_tokens = generation["response_tokens"]
|
|
@@ -160,7 +147,7 @@ def answer_question(
|
|
| 160 |
model_used = generation.get("model_used", "unknown")
|
| 161 |
fallback_used = generation["fallback_used"]
|
| 162 |
|
| 163 |
-
# Step 3
|
| 164 |
eval_scores = run_evaluation(
|
| 165 |
query=query,
|
| 166 |
answer=answer,
|
|
@@ -168,7 +155,7 @@ def answer_question(
|
|
| 168 |
retrieval_scores=scores,
|
| 169 |
)
|
| 170 |
|
| 171 |
-
# Step 4
|
| 172 |
record_query_metrics(
|
| 173 |
retrieval_latency_ms=retrieval["retrieval_latency_ms"],
|
| 174 |
generation_latency_ms=gen_latency,
|
|
@@ -178,7 +165,7 @@ def answer_question(
|
|
| 178 |
fallback_used=fallback_used,
|
| 179 |
)
|
| 180 |
|
| 181 |
-
# Step 5
|
| 182 |
trace_rag_query(
|
| 183 |
query=query,
|
| 184 |
answer=answer,
|
|
@@ -193,35 +180,30 @@ def answer_question(
|
|
| 193 |
fallback_used=fallback_used,
|
| 194 |
)
|
| 195 |
|
| 196 |
-
# ββ Format outputs
|
| 197 |
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
chunks_display = (
|
| 201 |
-
format_retrieved_chunks(retrieved_chunks, scores) + retrieval_warning
|
| 202 |
-
)
|
| 203 |
|
| 204 |
-
# Metrics panel
|
| 205 |
metrics_display = get_metrics_summary()
|
| 206 |
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
- Faithfulness: `{eval_scores['faithfulness']:.3f}`
|
| 210 |
-
- Answer Relevance: `{eval_scores['answer_relevance']:.3f}`
|
| 211 |
-
- Context Precision: `{eval_scores['context_precision']:.3f}`
|
| 212 |
-
- **Overall: `{eval_scores['overall']:.3f}`**
|
| 213 |
-
|
| 214 |
-
|
| 215 |
|
| 216 |
-
# Observability panel
|
| 217 |
obs_display = (
|
| 218 |
f"{get_observability_status()}\n\n"
|
| 219 |
-
f"**Last trace
|
| 220 |
-
f"
|
| 221 |
-
f"
|
| 222 |
-
f"
|
| 223 |
-
f"
|
| 224 |
-
f"
|
| 225 |
)
|
| 226 |
|
| 227 |
return answer, chunks_display, metrics_display, eval_display, obs_display
|
|
@@ -232,65 +214,64 @@ def answer_question(
|
|
| 232 |
|
| 233 |
|
| 234 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 235 |
-
# GRADIO UI
|
| 236 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 237 |
|
| 238 |
-
CSS = """
|
| 239 |
-
.panel-header { font-size: 13px; font-weight: 600; color: #666; }
|
| 240 |
-
.answer-box textarea { font-size: 15px !important; line-height: 1.7 !important; }
|
| 241 |
-
footer { display: none !important; }
|
| 242 |
-
"""
|
| 243 |
-
|
| 244 |
with gr.Blocks(
|
| 245 |
title="Enterprise RAG System",
|
| 246 |
theme=gr.themes.Soft(primary_hue="blue"),
|
| 247 |
-
css=CSS,
|
| 248 |
) as demo:
|
| 249 |
|
| 250 |
-
#
|
| 251 |
chunks_state = gr.State(None)
|
| 252 |
index_state = gr.State(None)
|
| 253 |
|
| 254 |
-
# ββ Header βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 255 |
gr.Markdown(
|
| 256 |
-
"
|
| 257 |
-
**RAG pipeline
|
| 258 |
-
""",
|
| 259 |
)
|
| 260 |
|
| 261 |
with gr.Row():
|
| 262 |
|
| 263 |
-
# ββ LEFT
|
| 264 |
-
with gr.Column(scale=1, min_width=
|
| 265 |
-
gr.Markdown("### π Document Upload"
|
| 266 |
|
| 267 |
pdf_input = gr.File(
|
| 268 |
label="Upload PDF",
|
| 269 |
file_types=[".pdf"],
|
| 270 |
-
type="filepath",
|
| 271 |
)
|
| 272 |
|
| 273 |
with gr.Accordion("βοΈ Chunking Settings", open=False):
|
| 274 |
chunk_size_slider = gr.Slider(
|
| 275 |
-
minimum=128,
|
|
|
|
|
|
|
|
|
|
| 276 |
label="Chunk Size (tokens)",
|
| 277 |
-
info="Larger = more context per chunk
|
| 278 |
)
|
| 279 |
chunk_overlap_slider = gr.Slider(
|
| 280 |
-
minimum=0,
|
|
|
|
|
|
|
|
|
|
| 281 |
label="Chunk Overlap (tokens)",
|
| 282 |
-
info="
|
| 283 |
)
|
| 284 |
top_k_slider = gr.Slider(
|
| 285 |
-
minimum=1,
|
|
|
|
|
|
|
|
|
|
| 286 |
label="Top-K Retrieval",
|
| 287 |
-
info="
|
| 288 |
)
|
| 289 |
|
| 290 |
process_btn = gr.Button("π₯ Process Document", variant="primary")
|
| 291 |
|
| 292 |
doc_status = gr.Textbox(
|
| 293 |
-
label="
|
| 294 |
lines=6,
|
| 295 |
interactive=False,
|
| 296 |
value="No document loaded.",
|
|
@@ -298,9 +279,9 @@ with gr.Blocks(
|
|
| 298 |
|
| 299 |
doc_info_md = gr.Markdown("")
|
| 300 |
|
| 301 |
-
# ββ CENTER
|
| 302 |
-
with gr.Column(scale=2, min_width=
|
| 303 |
-
gr.Markdown("### π¬
|
| 304 |
|
| 305 |
query_input = gr.Textbox(
|
| 306 |
label="Your Question",
|
|
@@ -311,46 +292,44 @@ with gr.Blocks(
|
|
| 311 |
ask_btn = gr.Button("π Get Answer", variant="primary", size="lg")
|
| 312 |
|
| 313 |
answer_output = gr.Markdown(
|
| 314 |
-
|
| 315 |
-
value="*Upload a document and ask a question to get started.*",
|
| 316 |
-
elem_classes="answer-box",
|
| 317 |
)
|
| 318 |
|
| 319 |
gr.Markdown(
|
| 320 |
-
"
|
| 321 |
-
**
|
| 322 |
-
-
|
| 323 |
-
-
|
| 324 |
-
-
|
| 325 |
-
""
|
| 326 |
)
|
| 327 |
|
| 328 |
-
# ββ RIGHT
|
| 329 |
-
with gr.Column(scale=1, min_width=
|
| 330 |
-
gr.Markdown("### π Observability
|
| 331 |
|
| 332 |
with gr.Tabs():
|
| 333 |
-
with gr.Tab("π
|
| 334 |
chunks_output = gr.Markdown(
|
| 335 |
-
value="*Retrieved context
|
| 336 |
)
|
| 337 |
|
| 338 |
with gr.Tab("π Metrics"):
|
| 339 |
metrics_output = gr.Markdown(
|
| 340 |
-
value="*Metrics
|
| 341 |
)
|
| 342 |
|
| 343 |
with gr.Tab("π§ͺ Evaluation"):
|
| 344 |
eval_output = gr.Markdown(
|
| 345 |
-
value="*Evaluation scores
|
| 346 |
)
|
| 347 |
|
| 348 |
with gr.Tab("π Traces"):
|
| 349 |
obs_output = gr.Markdown(
|
| 350 |
-
value=
|
| 351 |
)
|
| 352 |
|
| 353 |
-
# ββ Event
|
| 354 |
|
| 355 |
process_btn.click(
|
| 356 |
fn=process_pdf,
|
|
@@ -364,7 +343,6 @@ with gr.Blocks(
|
|
| 364 |
outputs=[answer_output, chunks_output, metrics_output, eval_output, obs_output],
|
| 365 |
)
|
| 366 |
|
| 367 |
-
# Allow pressing Enter in the query box
|
| 368 |
query_input.submit(
|
| 369 |
fn=answer_question,
|
| 370 |
inputs=[query_input, chunks_state, index_state, top_k_slider],
|
|
|
|
| 1 |
"""
|
| 2 |
+
app.py β Enterprise RAG System β Gradio 5 entry point.
|
| 3 |
|
| 4 |
+
Wires together all pipeline modules and renders the three-panel UI.
|
| 5 |
+
Uses gr.State() for per-session document isolation.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
"""
|
| 7 |
|
| 8 |
import os
|
|
|
|
| 17 |
from src.evaluation import run_evaluation
|
| 18 |
from src.observability import trace_rag_query, get_observability_status
|
| 19 |
from src.metrics import record_query_metrics, get_metrics_summary
|
| 20 |
+
from src.utils import format_retrieved_chunks
|
| 21 |
|
| 22 |
logging.basicConfig(level=logging.INFO)
|
| 23 |
logger = logging.getLogger("enterprise-rag.app")
|
| 24 |
|
| 25 |
+
|
| 26 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 27 |
+
# PIPELINE FUNCTIONS
|
| 28 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 29 |
|
| 30 |
def process_pdf(pdf_file, chunk_size: int, chunk_overlap: int):
|
| 31 |
"""
|
| 32 |
Full ingestion pipeline: PDF β text β chunks β embeddings β FAISS index.
|
| 33 |
|
| 34 |
+
Gradio 5 passes uploaded files as a file path string directly.
|
|
|
|
|
|
|
|
|
|
| 35 |
"""
|
| 36 |
if pdf_file is None:
|
| 37 |
+
return (
|
| 38 |
+
"β οΈ Please upload a PDF file first.",
|
| 39 |
+
None,
|
| 40 |
+
None,
|
| 41 |
+
"",
|
| 42 |
+
)
|
| 43 |
|
| 44 |
try:
|
| 45 |
+
# Gradio 5: pdf_file is a filepath string
|
| 46 |
+
file_path = pdf_file if isinstance(pdf_file, str) else pdf_file.name
|
| 47 |
+
|
| 48 |
+
with open(file_path, "rb") as f:
|
| 49 |
file_bytes = f.read()
|
| 50 |
|
| 51 |
+
# Validate size
|
| 52 |
valid, size_msg = validate_pdf(file_bytes)
|
| 53 |
if not valid:
|
| 54 |
+
return f"β {size_msg}", None, None, ""
|
| 55 |
|
| 56 |
+
# Step 1 β Extract text
|
| 57 |
extraction = extract_text_from_pdf(file_bytes)
|
| 58 |
if not extraction["success"]:
|
| 59 |
+
return f"β {extraction['error']}", None, None, ""
|
| 60 |
|
| 61 |
doc_text = extraction["text"]
|
| 62 |
page_count = extraction["page_count"]
|
| 63 |
|
| 64 |
+
# Step 2 β Chunk
|
| 65 |
chunks = chunk_text(doc_text, int(chunk_size), int(chunk_overlap))
|
| 66 |
if not chunks:
|
| 67 |
+
return "β No chunks created. Document may be too short or empty.", None, None, ""
|
| 68 |
|
| 69 |
stats = chunk_statistics(chunks)
|
| 70 |
chunk_texts = [c["text"] for c in chunks]
|
| 71 |
|
| 72 |
+
# Step 3 β Embed
|
| 73 |
embeddings = embed_texts(chunk_texts)
|
| 74 |
|
| 75 |
+
# Step 4 β Build FAISS index
|
| 76 |
faiss_index = build_faiss_index(embeddings)
|
| 77 |
|
| 78 |
status = (
|
| 79 |
f"β
Document processed successfully!\n\n"
|
| 80 |
f"π Pages: {page_count}\n"
|
| 81 |
f"π¦ Chunks: {stats['count']}\n"
|
| 82 |
+
f"π Avg chunk: {stats['avg_tokens']} tokens\n"
|
| 83 |
f"π’ Total tokens: {stats['total_tokens']}\n\n"
|
| 84 |
f"Ready to answer questions."
|
| 85 |
)
|
| 86 |
|
| 87 |
doc_info = (
|
| 88 |
+
f"**Loaded:** `{os.path.basename(file_path)}` \n"
|
| 89 |
f"Pages: {page_count} | Chunks: {stats['count']} | "
|
| 90 |
f"Avg chunk: {stats['avg_tokens']} tokens"
|
| 91 |
)
|
| 92 |
|
| 93 |
+
logger.info(
|
| 94 |
+
f"PDF ready: {stats['count']} chunks, "
|
| 95 |
+
f"{stats['total_tokens']} total tokens"
|
| 96 |
+
)
|
| 97 |
return status, chunks, faiss_index, doc_info
|
| 98 |
|
| 99 |
except Exception as e:
|
| 100 |
logger.error(f"PDF processing failed: {e}")
|
| 101 |
+
return f"β Error: {str(e)}", None, None, ""
|
| 102 |
|
| 103 |
|
| 104 |
+
def answer_question(query: str, chunks_state, index_state, top_k: int):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
"""
|
| 106 |
+
Full RAG query pipeline:
|
| 107 |
+
embed query β retrieve β generate β evaluate β trace β display.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
"""
|
|
|
|
|
|
|
| 109 |
if not query or not query.strip():
|
| 110 |
+
return "β οΈ Please enter a question.", "", "", "", ""
|
| 111 |
|
| 112 |
if chunks_state is None or index_state is None:
|
| 113 |
return (
|
| 114 |
"β οΈ No document loaded. Please upload a PDF first.",
|
| 115 |
+
"", "", "", "",
|
| 116 |
)
|
| 117 |
|
| 118 |
+
# Normalize chunks to plain text strings
|
| 119 |
chunk_texts = [
|
| 120 |
+
c["text"] if isinstance(c, dict) else c
|
| 121 |
+
for c in chunks_state
|
| 122 |
]
|
| 123 |
|
| 124 |
try:
|
| 125 |
+
# Step 1 β Retrieve
|
| 126 |
retrieval = retrieve_relevant_chunks(
|
| 127 |
query=query,
|
| 128 |
chunks=chunk_texts,
|
| 129 |
faiss_index=index_state,
|
| 130 |
top_k=int(top_k),
|
| 131 |
)
|
|
|
|
| 132 |
retrieved_chunks = retrieval["retrieved_chunks"]
|
| 133 |
scores = retrieval["scores"]
|
| 134 |
is_relevant = retrieval["is_relevant"]
|
| 135 |
|
| 136 |
+
# Step 2 β Generate
|
| 137 |
generation = generate_answer(
|
| 138 |
query=query,
|
| 139 |
context_chunks=retrieved_chunks,
|
| 140 |
scores=scores,
|
| 141 |
is_relevant=is_relevant,
|
| 142 |
)
|
|
|
|
| 143 |
answer = generation["answer"]
|
| 144 |
prompt_tokens = generation["prompt_tokens"]
|
| 145 |
response_tokens = generation["response_tokens"]
|
|
|
|
| 147 |
model_used = generation.get("model_used", "unknown")
|
| 148 |
fallback_used = generation["fallback_used"]
|
| 149 |
|
| 150 |
+
# Step 3 β Evaluate
|
| 151 |
eval_scores = run_evaluation(
|
| 152 |
query=query,
|
| 153 |
answer=answer,
|
|
|
|
| 155 |
retrieval_scores=scores,
|
| 156 |
)
|
| 157 |
|
| 158 |
+
# Step 4 β Record metrics
|
| 159 |
record_query_metrics(
|
| 160 |
retrieval_latency_ms=retrieval["retrieval_latency_ms"],
|
| 161 |
generation_latency_ms=gen_latency,
|
|
|
|
| 165 |
fallback_used=fallback_used,
|
| 166 |
)
|
| 167 |
|
| 168 |
+
# Step 5 β Trace
|
| 169 |
trace_rag_query(
|
| 170 |
query=query,
|
| 171 |
answer=answer,
|
|
|
|
| 180 |
fallback_used=fallback_used,
|
| 181 |
)
|
| 182 |
|
| 183 |
+
# ββ Format right-panel outputs βββββββββββββββββββββββββββββββββββββ
|
| 184 |
|
| 185 |
+
warning_text = f"\n\nβ οΈ {retrieval['warning']}" if retrieval.get("warning") else ""
|
| 186 |
+
chunks_display = format_retrieved_chunks(retrieved_chunks, scores) + warning_text
|
|
|
|
|
|
|
|
|
|
| 187 |
|
|
|
|
| 188 |
metrics_display = get_metrics_summary()
|
| 189 |
|
| 190 |
+
eval_display = (
|
| 191 |
+
f"**Answer Quality Scores**\n\n"
|
| 192 |
+
f"- Faithfulness: `{eval_scores['faithfulness']:.3f}` β grounded in context\n"
|
| 193 |
+
f"- Answer Relevance: `{eval_scores['answer_relevance']:.3f}` β answers the question\n"
|
| 194 |
+
f"- Context Precision: `{eval_scores['context_precision']:.3f}` β retrieval quality\n"
|
| 195 |
+
f"- **Overall: `{eval_scores['overall']:.3f}`** {eval_scores['quality_label']}\n\n"
|
| 196 |
+
f"{eval_scores.get('note', '')}"
|
| 197 |
+
)
|
| 198 |
|
|
|
|
| 199 |
obs_display = (
|
| 200 |
f"{get_observability_status()}\n\n"
|
| 201 |
+
f"**Last trace**\n"
|
| 202 |
+
f"- Model: `{model_used}`\n"
|
| 203 |
+
f"- Retrieval: `{retrieval['retrieval_latency_ms']:.0f}ms`\n"
|
| 204 |
+
f"- Generation: `{gen_latency:.0f}ms`\n"
|
| 205 |
+
f"- Total tokens: `{prompt_tokens + response_tokens}`\n"
|
| 206 |
+
f"- Fallback used: `{'Yes' if fallback_used else 'No'}`"
|
| 207 |
)
|
| 208 |
|
| 209 |
return answer, chunks_display, metrics_display, eval_display, obs_display
|
|
|
|
| 214 |
|
| 215 |
|
| 216 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 217 |
+
# GRADIO 5 UI
|
| 218 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 219 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 220 |
with gr.Blocks(
|
| 221 |
title="Enterprise RAG System",
|
| 222 |
theme=gr.themes.Soft(primary_hue="blue"),
|
|
|
|
| 223 |
) as demo:
|
| 224 |
|
| 225 |
+
# Per-session state β each user gets isolated chunks and FAISS index
|
| 226 |
chunks_state = gr.State(None)
|
| 227 |
index_state = gr.State(None)
|
| 228 |
|
|
|
|
| 229 |
gr.Markdown(
|
| 230 |
+
"# π’ Enterprise Knowledge Retrieval System\n"
|
| 231 |
+
"**RAG pipeline Β· Groq LLM Β· FAISS Β· Evaluation Β· Observability**"
|
|
|
|
| 232 |
)
|
| 233 |
|
| 234 |
with gr.Row():
|
| 235 |
|
| 236 |
+
# ββ LEFT: Document Upload βββββββββββββββββββββββββββββββββββββββββ
|
| 237 |
+
with gr.Column(scale=1, min_width=260):
|
| 238 |
+
gr.Markdown("### π Document Upload")
|
| 239 |
|
| 240 |
pdf_input = gr.File(
|
| 241 |
label="Upload PDF",
|
| 242 |
file_types=[".pdf"],
|
|
|
|
| 243 |
)
|
| 244 |
|
| 245 |
with gr.Accordion("βοΈ Chunking Settings", open=False):
|
| 246 |
chunk_size_slider = gr.Slider(
|
| 247 |
+
minimum=128,
|
| 248 |
+
maximum=1024,
|
| 249 |
+
value=512,
|
| 250 |
+
step=64,
|
| 251 |
label="Chunk Size (tokens)",
|
| 252 |
+
info="Larger = more context per chunk",
|
| 253 |
)
|
| 254 |
chunk_overlap_slider = gr.Slider(
|
| 255 |
+
minimum=0,
|
| 256 |
+
maximum=256,
|
| 257 |
+
value=64,
|
| 258 |
+
step=32,
|
| 259 |
label="Chunk Overlap (tokens)",
|
| 260 |
+
info="Prevents answer loss at boundaries",
|
| 261 |
)
|
| 262 |
top_k_slider = gr.Slider(
|
| 263 |
+
minimum=1,
|
| 264 |
+
maximum=10,
|
| 265 |
+
value=5,
|
| 266 |
+
step=1,
|
| 267 |
label="Top-K Retrieval",
|
| 268 |
+
info="Chunks returned per query",
|
| 269 |
)
|
| 270 |
|
| 271 |
process_btn = gr.Button("π₯ Process Document", variant="primary")
|
| 272 |
|
| 273 |
doc_status = gr.Textbox(
|
| 274 |
+
label="Status",
|
| 275 |
lines=6,
|
| 276 |
interactive=False,
|
| 277 |
value="No document loaded.",
|
|
|
|
| 279 |
|
| 280 |
doc_info_md = gr.Markdown("")
|
| 281 |
|
| 282 |
+
# ββ CENTER: Query & Answer ββββββββββββββββββββββββββββββββββββββββ
|
| 283 |
+
with gr.Column(scale=2, min_width=380):
|
| 284 |
+
gr.Markdown("### π¬ Ask Questions")
|
| 285 |
|
| 286 |
query_input = gr.Textbox(
|
| 287 |
label="Your Question",
|
|
|
|
| 292 |
ask_btn = gr.Button("π Get Answer", variant="primary", size="lg")
|
| 293 |
|
| 294 |
answer_output = gr.Markdown(
|
| 295 |
+
value="*Upload a document and ask a question to get started.*"
|
|
|
|
|
|
|
| 296 |
)
|
| 297 |
|
| 298 |
gr.Markdown(
|
| 299 |
+
"---\n"
|
| 300 |
+
"**Example questions after uploading:**\n"
|
| 301 |
+
"- What are the main topics covered?\n"
|
| 302 |
+
"- Summarize the key findings.\n"
|
| 303 |
+
"- What risks or challenges are mentioned?\n"
|
| 304 |
+
"- What are the specific numbers or statistics?"
|
| 305 |
)
|
| 306 |
|
| 307 |
+
# ββ RIGHT: Observability Panel ββββββββββββββββββββββββββββββββββββ
|
| 308 |
+
with gr.Column(scale=1, min_width=280):
|
| 309 |
+
gr.Markdown("### π Observability")
|
| 310 |
|
| 311 |
with gr.Tabs():
|
| 312 |
+
with gr.Tab("π Chunks"):
|
| 313 |
chunks_output = gr.Markdown(
|
| 314 |
+
value="*Retrieved context appears here after a query.*"
|
| 315 |
)
|
| 316 |
|
| 317 |
with gr.Tab("π Metrics"):
|
| 318 |
metrics_output = gr.Markdown(
|
| 319 |
+
value="*Metrics appear after the first query.*"
|
| 320 |
)
|
| 321 |
|
| 322 |
with gr.Tab("π§ͺ Evaluation"):
|
| 323 |
eval_output = gr.Markdown(
|
| 324 |
+
value="*Evaluation scores appear after a query.*"
|
| 325 |
)
|
| 326 |
|
| 327 |
with gr.Tab("π Traces"):
|
| 328 |
obs_output = gr.Markdown(
|
| 329 |
+
value=get_observability_status()
|
| 330 |
)
|
| 331 |
|
| 332 |
+
# ββ Event handlers ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 333 |
|
| 334 |
process_btn.click(
|
| 335 |
fn=process_pdf,
|
|
|
|
| 343 |
outputs=[answer_output, chunks_output, metrics_output, eval_output, obs_output],
|
| 344 |
)
|
| 345 |
|
|
|
|
| 346 |
query_input.submit(
|
| 347 |
fn=answer_question,
|
| 348 |
inputs=[query_input, chunks_state, index_state, top_k_slider],
|