rag-chat / app.py
HARISARAVANANM's picture
Upload app.py
7846a4d verified
import re, math, requests, gradio as gr
import json, os
from datetime import datetime
# ─────────────────────────────────────────────────────────────────────────────
# API CONFIG β€” add ONE secret in HF Space: Settings > Variables and secrets
# OPENROUTER_API_KEY β†’ your OpenRouter key
# ─────────────────────────────────────────────────────────────────────────────
OR_KEY = os.environ.get("OPENROUTER_API_KEY", "")
OR_URL = "https://openrouter.ai/api/v1/chat/completions"
OR_MODEL = "meta-llama/llama-3.3-70b-instruct:free" # stable free model on OpenRouter
# Embeddings run LOCALLY via sentence-transformers (no API key needed)
_embedder = None
def get_embedder():
global _embedder
if _embedder is None:
from sentence_transformers import SentenceTransformer
_embedder = SentenceTransformer("all-MiniLM-L6-v2")
return _embedder
# ── Global state ──────────────────────────────────────────────────────────────
store = []
chat_log = []
_uid = 0
def new_id():
global _uid; _uid += 1; return _uid
# ── Math ──────────────────────────────────────────────────────────────────────
def cosine(a, b):
d = sum(x*y for x,y in zip(a,b))
return d / (math.sqrt(sum(x*x for x in a)) * math.sqrt(sum(x*x for x in b)) + 1e-9)
# ── Embeddings (local sentence-transformers) ─────────────────────────────────
def embed(texts):
model = get_embedder()
vecs = model.encode(texts, convert_to_numpy=True).tolist()
return vecs if isinstance(vecs[0], list) else [vecs]
# ── Chat (OpenRouter) ────────────────────────────────────────────────────────
def llm(system_prompt, user_msg):
if not OR_KEY:
return "ERROR: OPENROUTER_API_KEY not set. Go to HF Space Settings > Variables and secrets > add OPENROUTER_API_KEY"
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {OR_KEY}",
"HTTP-Referer": "https://huggingface.co/spaces",
"X-Title": "RAG Neural Knowledge Interface"
}
payload = {
"model": OR_MODEL,
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_msg}
],
"max_tokens": 512,
"temperature": 0.4
}
# Try primary model, fall back to alternatives if 404
FALLBACK_MODELS = [
OR_MODEL,
"google/gemini-2.0-flash-exp:free",
"deepseek/deepseek-r1-distill-llama-8b",
"openrouter/auto",
]
last_err = None
for model in FALLBACK_MODELS:
try:
payload["model"] = model
r = requests.post(OR_URL, headers=headers, json=payload, timeout=60)
r.raise_for_status()
result = r.json()
if "choices" in result and result["choices"]:
return result["choices"][0]["message"]["content"].strip()
except Exception as e:
last_err = e
continue
return f"All models failed. Last error: {last_err}"
# ── Chunking ──────────────────────────────────────────────────────────────────
def chunks(text):
text = re.sub(r"\s+"," ",text).strip()
out, i = [], 0
while i < len(text):
out.append(text[i:i+450])
i += 370
return [c for c in out if len(c)>30]
# ── Ingest text ───────────────────────────────────────────────────────────────
def do_ingest(text, label):
if not text.strip(): return "Enter text first.", render_doc_table()
cs = chunks(text)
try: embs = embed(cs)
except Exception as e: return f"Embed error: {e}", render_doc_table()
src = label.strip() or f"doc-{new_id()}"
ts = datetime.now().strftime("%H:%M:%S")
for c,e in zip(cs,embs):
store.append({"t":c,"s":src,"e":e,"id":new_id(),"ts":ts})
return f"OK: {len(cs)} chunks from '{src}' | Total: {len(store)}", render_doc_table()
# ── Ingest URL ────────────────────────────────────────────────────────────────
def do_url(url):
if not url.strip(): return "Enter URL.", render_doc_table()
try:
r = requests.get(url.strip(), timeout=20, headers={"User-Agent":"Mozilla/5.0"})
r.raise_for_status()
txt = re.sub(r"<[^>]+>"," ",r.text)
return do_ingest(txt, url.strip()[:60])
except Exception as e:
return f"Fetch error: {e}", render_doc_table()
# ── Ingest PDF ────────────────────────────────────────────────────────────────
def do_pdf(file, label):
if file is None: return "Upload a PDF first.", render_doc_table()
try:
from pypdf import PdfReader
reader = PdfReader(file.name)
text = " ".join(page.extract_text() or "" for page in reader.pages)
src = label.strip() or file.name.split("/")[-1]
return do_ingest(text, src)
except Exception as e:
return f"PDF error: {e}", render_doc_table()
# ── Document manager ──────────────────────────────────────────────────────────
def render_doc_table():
if not store:
return "<div class='empty-state'>No documents ingested yet.</div>"
sources = {}
for item in store:
s = item["s"]
if s not in sources:
sources[s] = {"count":0,"ts":item["ts"]}
sources[s]["count"] += 1
rows = ""
for src, info in sources.items():
short = src[:35]+"..." if len(src)>35 else src
rows += f"<tr class='doc-row'><td class='doc-name' title='{src}'>{short}</td><td class='doc-chunks'>{info['count']}</td><td class='doc-time'>{info['ts']}</td></tr>"
return f"""<table class='doc-table'><thead><tr><th>Source</th><th>Chunks</th><th>Added</th></tr></thead><tbody>{rows}</tbody></table>
<div class='doc-footer'>Total: {len(store)} chunks across {len(sources)} source(s)</div>"""
def delete_source(src_name):
global store
before = len(store)
store = [x for x in store if x["s"] != src_name]
removed = before - len(store)
if removed == 0:
return f"Source '{src_name}' not found.", render_doc_table()
return f"Deleted '{src_name}' β€” removed {removed} chunks.", render_doc_table()
def clear_all_docs():
global store; store = []
return "All documents cleared.", render_doc_table()
# ── Retrieval ─────────────────────────────────────────────────────────────────
def retrieve(q, k):
if not store: return []
try: qe = embed([q])[0]
except: return []
return sorted([{**x,"sc":cosine(qe,x["e"])} for x in store],
key=lambda x:x["sc"], reverse=True)[:k]
# ── Chat respond ──────────────────────────────────────────────────────────────
def respond(msg, history, sys_p=None, topk=None, show_scores=None):
if sys_p is None:
sys_p = "You are a precise assistant. Answer ONLY from the provided context. If unsure, say so."
topk = int(topk) if topk is not None else 4
show_scores = bool(show_scores) if show_scores is not None else True
if not msg.strip(): return "Type a question."
hits = retrieve(msg, topk)
ctx = "\n\n".join(f"[{i+1}] {h['t']}" for i,h in enumerate(hits)) if hits else "No documents loaded β€” answer from general knowledge or say you don't know."
full_system = f"{sys_p}\n\nContext:\n{ctx}"
try:
ans = llm(full_system, msg)
except requests.HTTPError as e:
return f"API error: {e}"
except Exception as e:
return f"Error: {e}"
if hits and show_scores:
ans += " \n**Sources:** " + " | ".join(f"`{h['s'][:30]}` {h['sc']:.0%}" for h in hits)
chat_log.append({"time": datetime.now().isoformat(), "user": msg, "assistant": ans, "sources": [h["s"] for h in hits]})
return ans
# ── Export ────────────────────────────────────────────────────────────────────
def export_chat(fmt):
if not chat_log: return None, "No chat history to export."
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
if fmt == "JSON":
path = f"/tmp/rag_chat_{ts}.json"
with open(path,"w") as f: json.dump(chat_log, f, indent=2)
else:
path = f"/tmp/rag_chat_{ts}.md"
lines = [f"# RAG Chat Export - {ts}\n"]
for i,e in enumerate(chat_log,1):
lines += [f"## Turn {i} - {e['time']}", f"**User:** {e['user']}\n", f"**Assistant:** {e['assistant']}\n"]
if e['sources']: lines.append(f"*Sources: {', '.join(set(e['sources']))}*\n")
lines.append("---\n")
with open(path,"w") as f: f.write("\n".join(lines))
return path, f"Exported {len(chat_log)} turns."
def handle_export(fmt):
path, msg = export_chat(fmt)
return msg, gr.update(value=path, visible=bool(path))
def status():
srcs = len(set(x["s"] for x in store)) if store else 0
or_ok = "KEY SET" if OR_KEY else "NO KEY"
return f"{'LIVE' if store else 'IDLE'} | {len(store)} chunks | {srcs} sources | {len(chat_log)} turns | OpenRouter:{or_ok}"
def check_keys():
msgs = []
if not OR_KEY: msgs.append("OPENROUTER_API_KEY missing β€” add it in HF Space Settings > Variables and secrets")
if not msgs: return "All API keys configured correctly."
return "SETUP NEEDED:\n" + "\n".join(msgs)
# ═══════════════════════════════════════════════════════════════════════════════
# CSS
# ═══════════════════════════════════════════════════════════════════════════════
CSS = """
@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;600;700;900&family=JetBrains+Mono:wght@300;400;500&family=Rajdhani:wght@400;500;600;700&display=swap');
:root {
--void:#020408;--deep:#060d14;--panel:#0a1520;--border:#0e2035;--b2:#1a3550;
--plasma:#00d4ff;--ion:#7b2fff;--forge:#ff6b35;--nova:#00ff9d;--warn:#ffd700;
--text:#c8e0f0;--muted:#3a6080;--bright:#e8f4ff;
--fd:'Orbitron',monospace;--fb:'Rajdhani',sans-serif;--fm:'JetBrains Mono',monospace;
}
*,*::before,*::after{box-sizing:border-box;}
body,.gradio-container{background:var(--void)!important;color:var(--text)!important;font-family:var(--fb)!important;}
.gradio-container{max-width:100%!important;padding:0!important;}
footer{display:none!important;}
body::before{content:'';position:fixed;inset:0;z-index:0;pointer-events:none;
background:repeating-linear-gradient(0deg,transparent,transparent 2px,rgba(0,212,255,.012) 2px,rgba(0,212,255,.012) 4px);}
.rag-header{background:linear-gradient(135deg,#060d14 0%,#0a1a2e 50%,#060d14 100%);
border-bottom:1px solid var(--b2);padding:0 28px;height:58px;
display:flex;align-items:center;gap:16px;position:relative;overflow:hidden;}
.rag-header::after{content:'';position:absolute;bottom:0;left:0;right:0;height:1px;
background:linear-gradient(90deg,transparent,var(--plasma),var(--ion),transparent);}
.header-logo{font-family:var(--fd);font-size:11px;font-weight:700;color:var(--plasma);
letter-spacing:.3em;padding:4px 10px;border:1px solid var(--plasma);border-radius:3px;
box-shadow:0 0 12px rgba(0,212,255,.2),inset 0 0 8px rgba(0,212,255,.05);}
.header-title{font-family:var(--fd);font-size:18px;font-weight:900;
background:linear-gradient(135deg,var(--bright) 0%,var(--plasma) 60%,var(--ion) 100%);
-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;letter-spacing:.05em;}
.header-badge{margin-left:auto;font-family:var(--fm);font-size:9px;color:var(--nova);letter-spacing:.2em;display:flex;align-items:center;gap:6px;}
.pulse-dot{width:7px;height:7px;background:var(--nova);border-radius:50%;box-shadow:0 0 8px var(--nova);animation:pulse 2s infinite;}
@keyframes pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.4;transform:scale(.8)}}
.setup-banner{background:linear-gradient(135deg,#1a0a00,#2a1000);border:1px solid var(--forge);
border-radius:6px;padding:12px 16px;margin:8px 16px;font-family:var(--fm);font-size:10px;
color:var(--forge);line-height:1.8;letter-spacing:.05em;}
.setup-banner a{color:var(--warn);text-decoration:none;}
.setup-banner a:hover{text-decoration:underline;}
.sec-head{font-family:var(--fd);font-size:8px;letter-spacing:.3em;text-transform:uppercase;
color:var(--plasma);padding:12px 16px 8px;border-bottom:1px solid var(--border);
display:flex;align-items:center;gap:8px;background:var(--panel);}
.sec-head::before{content:'';width:4px;height:14px;background:linear-gradient(180deg,var(--plasma),var(--ion));border-radius:2px;flex-shrink:0;}
textarea,input[type=text],input[type=url]{
background:var(--deep)!important;border:1px solid var(--b2)!important;border-radius:4px!important;
color:var(--text)!important;font-family:var(--fm)!important;font-size:12px!important;
transition:border-color .2s,box-shadow .2s!important;}
textarea:focus,input[type=text]:focus{border-color:var(--plasma)!important;outline:none!important;
box-shadow:0 0 0 2px rgba(0,212,255,.12)!important;}
button.primary{background:linear-gradient(135deg,#0e2a50,#1a3a70)!important;border:1px solid var(--plasma)!important;
border-radius:4px!important;color:var(--plasma)!important;font-family:var(--fd)!important;
font-size:10px!important;font-weight:600!important;letter-spacing:.15em!important;
transition:all .2s!important;box-shadow:0 0 12px rgba(0,212,255,.1)!important;}
button.primary:hover{box-shadow:0 0 20px rgba(0,212,255,.3)!important;transform:translateY(-1px)!important;}
button.secondary{background:transparent!important;border:1px solid var(--border)!important;color:var(--muted)!important;
font-family:var(--fm)!important;font-size:10px!important;border-radius:4px!important;transition:all .2s!important;}
button.secondary:hover{border-color:var(--ion)!important;color:var(--ion)!important;box-shadow:0 0 12px rgba(123,47,255,.2)!important;}
.tab-nav button{background:transparent!important;border:none!important;border-bottom:2px solid transparent!important;
color:var(--muted)!important;font-family:var(--fm)!important;font-size:9px!important;
text-transform:uppercase!important;letter-spacing:.15em!important;padding:8px 12px!important;transition:all .2s!important;}
.tab-nav button.selected,.tab-nav button:hover{color:var(--plasma)!important;border-bottom-color:var(--plasma)!important;}
label span,.label-wrap span{color:var(--muted)!important;font-family:var(--fm)!important;font-size:9px!important;
text-transform:uppercase!important;letter-spacing:.12em!important;}
input[type=range]{accent-color:var(--plasma)!important;}
.accordion{background:var(--deep)!important;border:1px solid var(--border)!important;border-radius:4px!important;}
.doc-table{width:100%;border-collapse:collapse;font-family:var(--fm);font-size:10px;}
.doc-table th{color:var(--muted);font-size:9px;letter-spacing:.1em;text-transform:uppercase;
padding:6px 8px;border-bottom:1px solid var(--border);text-align:left;}
.doc-row{border-bottom:1px solid var(--border);transition:background .15s;}
.doc-row:hover{background:rgba(0,212,255,.04);}
.doc-name{color:var(--text);padding:6px 8px;max-width:160px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.doc-chunks{color:var(--plasma);padding:6px 8px;text-align:center;}
.doc-time{color:var(--muted);padding:6px 8px;}
.doc-footer{font-family:var(--fm);font-size:9px;color:var(--muted);padding:6px 8px;border-top:1px solid var(--border);text-align:right;}
.empty-state{font-family:var(--fm);font-size:10px;color:var(--muted);text-align:center;padding:20px;}
::-webkit-scrollbar{width:4px;height:4px;}
::-webkit-scrollbar-track{background:var(--void);}
::-webkit-scrollbar-thumb{background:var(--b2);border-radius:2px;}
::-webkit-scrollbar-thumb:hover{background:var(--plasma);}
"""
# ═══════════════════════════════════════════════════════════════════════════════
# LAYOUT
# ═══════════════════════════════════════════════════════════════════════════════
with gr.Blocks(title="RAG - Neural Knowledge Interface") as demo:
gr.HTML("""
<div class="rag-header">
<div class="header-logo">RAG</div>
<div class="header-title">NEURAL KNOWLEDGE INTERFACE</div>
<div class="header-badge"><span class="pulse-dot"></span>OPENROUTER + HF EMBEDDINGS</div>
</div>
""")
# Setup banner shown when keys missing
key_status = check_keys()
if "SETUP NEEDED" in key_status:
gr.HTML(f"""
<div class="setup-banner">
{key_status.replace(chr(10),'<br>')}
<br><br>
STEP 1: In your HF Space β†’ <b>Settings</b> β†’ <b>Variables and secrets</b><br>
STEP 2: Add secret: <b>OPENROUTER_API_KEY</b> = your OpenRouter key<br>
STEP 3: Click <b>Restart Space</b> β€” done!
</div>
""")
with gr.Row(equal_height=True):
# ── LEFT: Chat ────────────────────────────────────────────────────
with gr.Column(scale=3):
gr.HTML('<div class="sec-head">CONVERSATION INTERFACE</div>')
gr.ChatInterface(
fn=respond,
chatbot=gr.Chatbot(
height=480, show_label=False,
placeholder="<div style='text-align:center;padding:60px 0;font-family:JetBrains Mono,monospace;color:#0e2035;font-size:11px;letter-spacing:.1em'>INGEST DOCUMENTS FIRST<br><br><span style='font-size:9px'>AWAITING KNOWLEDGE BASE INITIALIZATION</span></div>",
),
additional_inputs=[
gr.Textbox(value="You are a precise assistant. Answer ONLY from the provided context. If unsure, say so.", label="System Prompt", lines=2),
gr.Slider(1, 8, value=4, step=1, label="Top-K Chunks"),
gr.Checkbox(value=True, label="Show Source Scores"),
],
additional_inputs_accordion=gr.Accordion(label="Advanced Settings", open=False),
examples=[
["What is this document about?"],
["Summarise the key points"],
["What are the main conclusions?"],
["List all facts mentioned"],
],
)
with gr.Row():
status_box = gr.Textbox(value=status(), show_label=False, interactive=False, scale=4, container=False)
gr.Button("REFRESH", variant="secondary", scale=1).click(status, outputs=status_box)
# ── RIGHT: Sidebar ────────────────────────────────────────────────
with gr.Column(scale=1, min_width=340):
gr.HTML('<div class="sec-head">KNOWLEDGE INGESTION</div>')
with gr.Tabs():
with gr.Tab("Text"):
t_box = gr.Textbox(placeholder="Paste articles, notes, research...", lines=6, show_label=False)
t_lbl = gr.Textbox(placeholder="Source label (e.g. paper-2024)", show_label=False)
t_btn = gr.Button("INGEST TEXT", variant="primary")
t_res = gr.Markdown()
with gr.Tab("URL"):
u_box = gr.Textbox(placeholder="https://en.wikipedia.org/...", show_label=False)
u_btn = gr.Button("FETCH & INGEST", variant="primary")
u_res = gr.Markdown()
with gr.Tab("PDF"):
p_file = gr.File(label="Upload PDF", file_types=[".pdf"])
p_lbl = gr.Textbox(placeholder="Source label (optional)", show_label=False)
p_btn = gr.Button("PARSE & INGEST", variant="primary")
p_res = gr.Markdown()
gr.HTML('<div class="sec-head">DOCUMENT MANAGER</div>')
doc_table = gr.HTML(value=render_doc_table())
with gr.Row():
del_src = gr.Textbox(placeholder="Exact source name to delete", show_label=False, scale=3)
del_btn = gr.Button("DELETE", variant="secondary", scale=1)
del_res = gr.Markdown()
clr_btn = gr.Button("CLEAR ALL DOCUMENTS", variant="secondary")
clr_res = gr.Markdown()
gr.HTML('<div class="sec-head">EXPORT CHAT HISTORY</div>')
with gr.Row():
exp_fmt = gr.Radio(["JSON","Markdown"], value="Markdown", label="Format")
exp_btn = gr.Button("EXPORT", variant="primary")
exp_res = gr.Markdown()
exp_file = gr.File(label="Download", visible=False)
gr.HTML("""
<div style="margin:12px 16px;padding:10px 14px;background:linear-gradient(135deg,#020408,#060d14);
border:1px solid #0e2035;border-radius:4px;font-family:'JetBrains Mono',monospace;
font-size:9px;color:#1a3550;line-height:2.2;letter-spacing:.05em">
INFERENCE | OpenRouter / mistral-7b-instruct:free<br>
EMBEDDINGS | local / all-MiniLM-L6-v2<br>
VECTOR DB | in-memory cosine similarity<br>
SECRET | OPENROUTER_API_KEY
</div>""")
t_btn.click(do_ingest, [t_box, t_lbl], [t_res, doc_table])
u_btn.click(do_url, [u_box], [u_res, doc_table])
p_btn.click(do_pdf, [p_file, p_lbl], [p_res, doc_table])
del_btn.click(delete_source, [del_src], [del_res, doc_table])
clr_btn.click(clear_all_docs, [], [clr_res, doc_table])
exp_btn.click(handle_export, [exp_fmt], [exp_res, exp_file])
demo.launch(css=CSS)