# ui.py import gradio as gr import html from datetime import date as _date from app import ( add_doc_endpoint, ask_endpoint, metrics_endpoint, DocInput, QuestionInput, ) # Helpers def _parse_answer_sections(answer_text: str): lines = [l.strip() for l in (answer_text or "").splitlines() if l.strip()] out = { "main": "", "citations": "", "graph_reasoning": "", "confidence": "", "knobs": "", "knobs_explain": "", } main_parts = [] for ln in lines: ll = ln.lower() if ll.startswith("citations:"): out["citations"] = ln.split(":", 1)[1].strip() elif ll.startswith("graph reasoning:") or ll.startswith("graphreasoning:"): out["graph_reasoning"] = ln.split(":", 1)[1].strip() elif ll.startswith("confidence:"): out["confidence"] = ln.split(":", 1)[1].strip() elif ll.startswith("knobs explain:"): out["knobs_explain"] = ln.split(":", 1)[1].strip() elif ll.startswith("knobs:"): out["knobs"] = ln.split(":", 1)[1].strip() else: main_parts.append(ln) out["main"] = " ".join(main_parts).strip() or (answer_text or "").strip() return out def _confidence_class(conf: str) -> str: c = (conf or "").strip().lower() if c.startswith("high"): return "badge-high" if c.startswith("medium"): return "badge-medium" if c.startswith("low"): return "badge-low" return "badge-none" def _render_answer_card(answer_text: str) -> str: sec = _parse_answer_sections(answer_text) conf_cls = _confidence_class(sec["confidence"]) main = html.escape(sec["main"]) citations = html.escape(sec["citations"] or "None") greason = html.escape(sec["graph_reasoning"] or "β€”") conf = html.escape(sec["confidence"] or "β€”") knobs = html.escape(sec["knobs"] or "β€”") knobs_explain = html.escape(sec["knobs_explain"] or "β€”") return f"""
Answer
{main}
{conf}
Citations: {citations}
Graph reasoning: {greason}
Knobs effect: {knobs}
Knobs explain: {knobs_explain}
""" def _render_evidence_markdown(evidence_list): if not evidence_list: return "_No evidence returned._" lines = [] for i, chunk in enumerate(evidence_list, 1): chunk = chunk.strip() lines.append(f"**E{i}.** {chunk}") return "\n\n".join(lines) def _wrap_svg(svg: str) -> str: if not svg or "No graph" return f"""
{svg}
""" # direct function calls, no HTTP def metrics_ui(): try: j = metrics_endpoint() if j.get("status") != "ok": return f"Error: {j}" r = j["results"] return f""" ### πŸ“Š Evaluation Results **Baseline (cosine-only)** - hit@10: {r['baseline']['hit@10']:.2f} - nDCG@10: {r['baseline']['nDCG@10']:.2f} **Hybrid (GraphRAG)** - hit@10: {r['hybrid']['hit@10']:.2f} - nDCG@10: {r['hybrid']['nDCG@10']:.2f} **Other** - Citation correctness: {r['citation_correctness']:.2f} - Avg latency (s): {r['avg_latency_sec']:.2f} """ except Exception as e: return f"Error: {e}" def add_doc_ui(text, source="user", date_val=None, time_val=None): # Build ISO timestamp if a date was picked ts_iso = "" if date_val: # gr.Date may return a datetime.date or a 'YYYY-MM-DD' string if isinstance(date_val, _date): dstr = date_val.isoformat() else: dstr = str(date_val) # time_val can be None, "HH:MM" (gr.Time) or "HH:MM:SS" tstr = (time_val or "00:00").strip() if len(tstr) == 5: # HH:MM -> add seconds tstr = f"{tstr}:00" ts_iso = f"{dstr}T{tstr}Z" try: doc = DocInput(text=text, source=source, timestamp=ts_iso or None) j = add_doc_endpoint(doc) return "\n".join(j.get("logs", [])) or "No logs." except Exception as e: return f"Error: {e}" def ask_ui(question, w_cos, w_path, w_fresh, w_deg): try: q = QuestionInput( question=question, w_cos=w_cos, w_path=w_path, w_fresh=w_fresh, w_deg=w_deg, ) j = ask_endpoint(q) except Exception as e: err = f"Error: {e}" return ( _render_answer_card("I don’t know based on the given evidence.\nConfidence: Low"), "_No evidence returned._", err, "
", {}, ) answer_html = _render_answer_card(j.get("answer", "")) evidence_md = _render_evidence_markdown(j.get("evidence", [])) logs_txt = "\n".join(j.get("logs", [])) or "No logs." # Prefer D3 container fall back to server SVG graph_json = j.get("subgraph_json", {}) if graph_json and graph_json.get("nodes"): graph_html_value = "
" else: graph_html_value = _wrap_svg(j.get("subgraph_svg", "")) return (answer_html, evidence_md, logs_txt, graph_html_value, graph_json) # UI with gr.Blocks( css=""" /* Layout & theme */ body { background: #0b0f14; color: #e6edf3; } .gradio-container { max-width: 1180px !important; } .section-title { font-size: 22px; font-weight: 700; margin: 6px 0 12px; } /* Cards */ .card { background: #0f1720; border: 1px solid #1f2a36; border-radius: 14px; padding: 14px; } .card-title { font-size: 16px; letter-spacing: .3px; color: #9fb3c8; margin-bottom: 8px; text-transform: uppercase; } .answer { font-size: 18px; line-height: 1.5; margin-bottom: 8px; } .sub { color: #a8b3bf; margin-top: 6px; font-size: 14px; } /* Badges */ .badge { padding: 3px 10px; border-radius: 999px; font-size: 12px; font-weight: 700; display: inline-block; } .badge-high { background: #12391a; color: #6ee787; border: 1px solid #285f36; } .badge-medium { background: #3a2b13; color: #ffd277; border: 1px solid #6b4e1f; } .badge-low { background: #3b1616; color: #ff9492; border: 1px solid #6b2020; } .badge-none { background: #223; color: #9fb3c8; border: 1px solid #334; } /* Graph */ .graph-wrap { background: #0f1720; border: 1px solid #1f2a36; border-radius: 14px; padding: 12px; height: 460px; overflow: auto; } .graph-empty { color: #9fb3c8; font-style: italic; padding: 16px; } /* Logs */ #logs-box textarea { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace !important; max-height: 280px !important; overflow-y: auto !important; } """ ) as demo: gr.Markdown("### πŸš€ GraphRAG β€” Live Demo") with gr.Tab("Add Document"): with gr.Row(): with gr.Column(scale=3): text_in = gr.Textbox( label="Document", lines=10, placeholder="Paste text to inject into Graph + Vector DB…", ) with gr.Column(scale=1): source_in = gr.Textbox(label="Source", value="user") # Date & Time pickers if hasattr(gr, "Date"): ts_date = gr.Date(label="Date (optional)") else: ts_date = gr.Textbox(label="Date (YYYY-MM-DD, optional)") if hasattr(gr, "Time"): ts_time = gr.Time(label="Time (optional)", value="00:00") else: ts_time = gr.Textbox(label="Time (HH:MM, optional)", value="00:00") add_btn = gr.Button("Add Doc", variant="primary") add_logs = gr.Textbox(label="Ingestion Logs", lines=14, elem_id="logs-box") add_btn.click( add_doc_ui, inputs=[text_in, source_in, ts_date, ts_time], outputs=add_logs ) with gr.Tab("Ask Question"): with gr.Row(): q_in = gr.Textbox( label="Question", placeholder="e.g., Who acquired Instagram?" ) ask_btn = gr.Button("Ask", variant="primary") with gr.Accordion("Rerank Weights", open=False): w_cos = gr.Slider(0, 1, value=0.60, step=0.05, label="Cosine weight") w_path = gr.Slider(0, 1, value=0.20, step=0.05, label="Path proximity weight") w_fresh = gr.Slider(0, 1, value=0.15, step=0.05, label="Freshness weight") w_deg = gr.Slider(0, 1, value=0.05, step=0.05, label="Degree norm weight") with gr.Row(): with gr.Column(scale=1): gr.Markdown("
Answer
") ans_html = gr.HTML(value=_render_answer_card("Ask something to see results.")) evid = gr.Accordion("Evidence (ranked)", open=True) with evid: evid_md = gr.Markdown() logs = gr.Accordion("Debug logs", open=False) with logs: logs_txt = gr.Textbox(lines=14, elem_id="logs-box") with gr.Column(scale=1): gr.Markdown("
Evidence Graph
") graph_html = gr.HTML(value="
") graph_data = gr.JSON(label="graph-data", visible=False) # ask -> 5 outputs (answer, evidence, logs, graph container, graph JSON) ask_btn.click( ask_ui, inputs=[q_in, w_cos, w_path, w_fresh, w_deg], outputs=[ans_html, evid_md, logs_txt, graph_html, graph_data], ) with gr.Tab("Metrics"): metrics_btn = gr.Button("Run Evaluation", variant="primary") metrics_out = gr.Markdown("Click run to evaluate baseline vs hybrid.") metrics_btn.click(metrics_ui, inputs=[], outputs=metrics_out) # D3 renderer (zoom + pan) DRAW_JS = r""" (value) => { const el = document.querySelector("#graph"); if (!el) return null; el.innerHTML = ""; if (!value || !value.nodes || value.nodes.length === 0) { el.innerHTML = "
No graph
"; return null; } function ensureD3(cb) { if (window.d3) return cb(); const s = document.createElement("script"); s.src = "https://cdn.jsdelivr.net/npm/d3@7"; s.onload = cb; document.head.appendChild(s); } ensureD3(() => { const width = el.clientWidth || 900; const height = 600; const svg = d3.select(el).append("svg") .attr("viewBox", [0, 0, width, height]) .attr("preserveAspectRatio", "xMidYMid meet") .style("width", "100%") .style("height", "100%"); // Zoomable container const container = svg.append("g"); // Enable zoom & pan svg.call( d3.zoom() .scaleExtent([0.2, 3]) .on("zoom", (event) => { container.attr("transform", event.transform); }) ); const sim = d3.forceSimulation(value.nodes) .force("link", d3.forceLink(value.links).id(d => d.id).distance(140).strength(0.4)) .force("charge", d3.forceManyBody().strength(-220)) .force("center", d3.forceCenter(width / 2, height / 2)); const link = container.append("g") .attr("stroke", "#999") .attr("stroke-opacity", 0.6) .selectAll("line") .data(value.links) .enter().append("line") .attr("stroke-width", 1.5); const edgeLabels = container.append("g") .selectAll("text") .data(value.links) .enter().append("text") .attr("font-size", 10) .attr("fill", "#bbb") .text(d => d.label); const node = container.append("g") .selectAll("circle") .data(value.nodes) .enter().append("circle") .attr("r", 12) .attr("fill", "#69b3a2") .attr("stroke", "#2dd4bf") .attr("stroke-width", 1.2) .call(d3.drag() .on("start", (event, d) => { if (!event.active) sim.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; }) .on("drag", (event, d) => { d.fx = event.x; d.fy = event.y; }) .on("end", (event, d) => { if (!event.active) sim.alphaTarget(0); d.fx = null; d.fy = null; }) ); const labels = container.append("g") .selectAll("text") .data(value.nodes) .enter().append("text") .attr("font-size", 12) .attr("fill", "#ddd") .attr("dy", 18) .attr("text-anchor", "middle") .text(d => d.id); sim.on("tick", () => { link .attr("x1", d => d.source.x) .attr("y1", d => d.source.y) .attr("x2", d => d.target.x) .attr("y2", d => d.target.y); edgeLabels .attr("x", d => (d.source.x + d.target.x) / 2) .attr("y", d => (d.source.y + d.target.y) / 2); node .attr("cx", d => d.x) .attr("cy", d => d.y); labels .attr("x", d => d.x) .attr("y", d => d.y); }); }); return null; } """ graph_data.change(lambda x: x, inputs=graph_data, outputs=graph_data).then( None, inputs=graph_data, outputs=None, js=DRAW_JS ) if __name__ == "__main__": demo.launch()