RAG_Wikipedia / app.py
AaronTekle's picture
Update app.py
67d1a40 verified
Raw
History Blame Contribute Delete
14.6 kB
"""user interface for RAG"""
from __future__ import annotations
import os
import threading
from typing import Any
import gradio as gr
import spaces
from config import SETTINGS
from rag_engine import RAGEngine
ENGINE = RAGEngine()
ENGINE_LOCK = threading.Lock()
CSS = r"""
:root {
--surface: rgba(11, 17, 31, .76);
--surface-2: rgba(20, 29, 50, .72);
--line: rgba(148, 163, 184, .16);
--muted: #9ba9bd;
--text: #f7f9fc;
--accent: #8b5cf6;
--accent-2: #22d3ee;
}
.gradio-container {
max-width: 1480px !important;
margin: 0 auto !important;
color: var(--text) !important;
background:
radial-gradient(
circle at 8% 8%,
rgba(139, 92, 246, .20),
transparent 29%
),
radial-gradient(
circle at 91% 12%,
rgba(34, 211, 238, .14),
transparent 26%
),
linear-gradient(
145deg,
#050811 0%,
#080d19 46%,
#0b1020 100%
) !important;
min-height: 100vh;
}
.main-shell {
padding: 28px 24px 44px;
}
.hero {
position: relative;
overflow: hidden;
border: 1px solid var(--line);
background:
linear-gradient(
135deg,
rgba(19, 27, 48, .94),
rgba(10, 15, 29, .84)
);
border-radius: 24px;
padding: 30px 32px;
box-shadow: 0 28px 80px rgba(0, 0, 0, .28);
margin-bottom: 18px;
}
.hero::after {
content: "";
position: absolute;
width: 330px;
height: 330px;
right: -110px;
top: -160px;
background:
radial-gradient(
circle,
rgba(34, 211, 238, .22),
transparent 65%
);
}
.eyebrow {
color: #b8a6ff;
font-size: 12px;
font-weight: 800;
letter-spacing: .16em;
text-transform: uppercase;
}
.hero h1 {
margin: 8px 0 7px;
font-size: clamp(32px, 5vw, 57px);
line-height: 1.02;
letter-spacing: -.045em;
}
.hero p {
max-width: 850px;
color: #b8c3d4;
font-size: 16px;
line-height: 1.65;
margin: 0;
}
.badges {
display: flex;
flex-wrap: wrap;
gap: 9px;
margin-top: 18px;
}
.badge {
border: 1px solid var(--line);
background: rgba(255, 255, 255, .035);
padding: 7px 11px;
border-radius: 999px;
color: #cbd5e1;
font-size: 12px;
}
.badge strong {
color: #fff;
}
.panel,
.gr-panel,
.block {
border-color: var(--line) !important;
}
.app-panel {
background: var(--surface) !important;
border: 1px solid var(--line) !important;
border-radius: 20px !important;
box-shadow: 0 18px 48px rgba(0, 0, 0, .20);
}
.sidebar-card {
background: var(--surface-2);
border: 1px solid var(--line);
border-radius: 18px;
padding: 18px;
margin-bottom: 14px;
}
.sidebar-card h3 {
margin: 0 0 8px;
font-size: 14px;
}
.sidebar-card p {
margin: 0;
color: var(--muted);
font-size: 13px;
line-height: 1.55;
}
#chatbot {
min-height: 520px;
}
#chatbot .message {
border-radius: 16px !important;
}
#prompt textarea {
font-size: 15px !important;
line-height: 1.5 !important;
}
#send-button {
min-width: 115px;
font-weight: 800;
}
.source-list {
display: grid;
gap: 10px;
}
.source-card {
border: 1px solid var(--line);
border-radius: 14px;
background: rgba(255, 255, 255, .025);
overflow: hidden;
}
.source-card summary {
cursor: pointer;
list-style: none;
display: grid;
grid-template-columns: 28px 1fr auto;
gap: 9px;
align-items: center;
padding: 12px 13px;
}
.source-card summary::-webkit-details-marker {
display: none;
}
.source-number {
display: grid;
place-items: center;
width: 24px;
height: 24px;
border-radius: 8px;
background:
linear-gradient(
135deg,
var(--accent),
var(--accent-2)
);
color: white;
font-size: 11px;
font-weight: 900;
}
.source-title {
color: #e5eaf2;
font-size: 13px;
font-weight: 700;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.source-score {
font-size: 11px;
color: #8cdff0;
font-variant-numeric: tabular-nums;
}
.source-body {
padding: 0 14px 12px 50px;
color: #b7c2d4;
font-size: 12px;
line-height: 1.65;
}
.source-meta {
padding: 9px 14px;
border-top: 1px solid var(--line);
color: #75859c;
font-size: 10px;
}
.empty-state {
color: var(--muted);
padding: 18px;
text-align: center;
border: 1px dashed var(--line);
border-radius: 14px;
}
.footer-note {
color: #718097;
font-size: 11px;
text-align: center;
margin-top: 16px;
}
button.primary {
background:
linear-gradient(
135deg,
#7c3aed,
#0891b2
) !important;
border: none !important;
}
.accordion {
background: rgba(255, 255, 255, .02) !important;
border-color: var(--line) !important;
}
@media (max-width: 800px) {
.main-shell {
padding: 14px 10px 28px;
}
.hero {
padding: 23px 20px;
border-radius: 18px;
}
#chatbot {
min-height: 430px;
}
}
"""
HEAD = """
<meta name="theme-color" content="#070b15">
<meta
name="description"
content="Hybrid retrieval-augmented generation with Hugging Face models and datasets."
>
"""
def get_engine() -> RAGEngine:
"""Initialize and return the shared RAG engine."""
if not ENGINE.ready:
with ENGINE_LOCK:
if not ENGINE.ready:
ENGINE.initialize()
return ENGINE
@spaces.GPU(duration=60)
def run_chat(
message: str,
history: list[dict[str, Any]] | None,
top_k: int,
dense_weight: float,
use_reranker: bool,
temperature: float,
max_tokens: int,
):
"""Process a question through the RAG pipeline."""
clean_message = (message or "").strip()
history = list(history or [])
if not clean_message:
return (
history,
"<div class='empty-state'>Enter a question first.</div>",
{},
"",
)
engine = get_engine()
prior_history = [
{
"role": str(item.get("role")),
"content": str(item.get("content", "")),
}
for item in history
if isinstance(item, dict)
]
answer, results = engine.answer(
query=clean_message,
history=prior_history,
top_k=int(top_k),
dense_weight=float(dense_weight),
use_reranker=bool(use_reranker),
temperature=float(temperature),
max_tokens=int(max_tokens),
)
history.extend(
[
{
"role": "user",
"content": clean_message,
},
{
"role": "assistant",
"content": answer,
},
]
)
return (
history,
engine.render_sources(results),
engine.diagnostics(results),
"",
)
def clear_all():
"""Clear the conversation and retrieved evidence."""
return (
[],
"<div class='empty-state'>No sources retrieved yet.</div>",
{},
"",
)
def build_app() -> gr.Blocks:
"""Build and return the Gradio application."""
token_state = "configured" if SETTINGS.hf_token else "missing"
hero = f"""
<div class="hero">
<div class="eyebrow">RAG</div>
<h1>RAG with Wikipedia</h1>
<p>
Ask an RAG model questions trained on
<code>rag-datasets/rag-mini-wikipedia</code>.
</p>
<div class="badges">
<span class="badge">
<strong>Generator</strong>
Qwen3 4B Instruct
</span>
<span class="badge">
<strong>Embeddings</strong>
MiniLM-L6
</span>
<span class="badge">
<strong>Dataset</strong>
rag-mini-wikipedia
</span>
<span class="badge">
<strong>HF token</strong>
{token_state}
</span>
</div>
</div>
"""
with gr.Blocks(title="RAG Wikipedia") as demo:
with gr.Column(elem_classes=["main-shell"]):
gr.HTML(hero)
with gr.Row(equal_height=False):
with gr.Column(
scale=8,
min_width=520,
elem_classes=["app-panel"],
):
chatbot = gr.Chatbot(
value=[],
label="Grounded conversation",
elem_id="chatbot",
height=565,
buttons=["copy"],
placeholder=(
"Ask a factual question about the indexed "
"Wikipedia passages."
),
)
with gr.Row():
prompt = gr.Textbox(
label="Question",
placeholder=(
"Example: What did the Legal Tender Act "
"of 1862 establish?"
),
lines=2,
max_lines=5,
elem_id="prompt",
scale=8,
)
send = gr.Button(
"Ask",
variant="primary",
elem_id="send-button",
scale=1,
)
with gr.Row():
clear = gr.Button("Clear conversation")
gr.Examples(
examples=[
(
"What did the Legal Tender Act of 1862 "
"establish?"
),
"What is Amedeo Avogadro most noted for?",
"What is the study of beetles called?",
(
"What happened to the Celsius temperature "
"scale in 1745?"
),
],
inputs=prompt,
label="Suggested questions",
)
with gr.Column(
scale=5,
min_width=360,
):
gr.HTML(
"""
<div class="sidebar-card">
<h3>Retrieved evidence</h3>
<p>
Answers use numbered citations that correspond
to the passages below. Open a source to inspect
the exact evidence.
</p>
</div>
"""
)
sources = gr.HTML(
(
"<div class='empty-state'>"
"No sources retrieved yet."
"</div>"
),
label="Sources",
elem_classes=["app-panel"],
)
with gr.Accordion(
"Retrieval controls",
open=True,
elem_classes=["accordion"],
):
top_k = gr.Slider(
minimum=3,
maximum=8,
value=5,
step=1,
label="Sources returned",
)
dense_weight = gr.Slider(
minimum=0.0,
maximum=1.0,
value=0.72,
step=0.05,
label="Semantic retrieval weight",
info=(
"The remaining weight is assigned to "
"TF-IDF lexical retrieval."
),
)
use_reranker = gr.Checkbox(
value=True,
label="Use neural reranker",
)
with gr.Accordion(
"Generation controls",
open=False,
elem_classes=["accordion"],
):
temperature = gr.Slider(
minimum=0.0,
maximum=1.0,
value=0.2,
step=0.05,
label="Temperature",
)
max_tokens = gr.Slider(
minimum=200,
maximum=1200,
value=700,
step=50,
label="Maximum output tokens",
)
with gr.Accordion(
"Diagnostics",
open=False,
elem_classes=["accordion"],
):
diagnostics = gr.JSON(
value={},
label="Pipeline diagnostics",
)
gr.HTML(
"""
<div class="footer-note">
Retrieval runs locally. Generation uses Hugging Face
Inference Providers and requires an HF token with
available credits.
</div>
"""
)
inputs = [
prompt,
chatbot,
top_k,
dense_weight,
use_reranker,
temperature,
max_tokens,
]
outputs = [
chatbot,
sources,
diagnostics,
prompt,
]
send.click(
fn=run_chat,
inputs=inputs,
outputs=outputs,
)
prompt.submit(
fn=run_chat,
inputs=inputs,
outputs=outputs,
)
clear.click(
fn=clear_all,
outputs=outputs,
)
return demo
if __name__ == "__main__":
app = build_app()
app.queue(
default_concurrency_limit=4,
).launch(
server_name="0.0.0.0",
server_port=int(os.getenv("PORT", "7860")),
show_error=True,
theme=gr.themes.Base(),
css=CSS,
head=HEAD,
)