Spaces:
Sleeping
Sleeping
| 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) | |