azlaan428 commited on
Commit ·
20b4913
1
Parent(s): 58d2397
feat: SSE streaming pipeline, progress bar, PDF export
Browse files- app.py +60 -6
- templates/index.html +70 -72
app.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
-
import sys, os
|
| 2 |
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
| 3 |
-
from flask import Flask, render_template, request, jsonify, send_file
|
| 4 |
-
from agent.agent import run_pipeline
|
| 5 |
from reportlab.lib.pagesizes import A4
|
| 6 |
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
| 7 |
from reportlab.lib.units import mm
|
|
@@ -12,10 +12,12 @@ import io
|
|
| 12 |
|
| 13 |
app = Flask(__name__)
|
| 14 |
|
|
|
|
| 15 |
@app.route("/")
|
| 16 |
def index():
|
| 17 |
return render_template("index.html")
|
| 18 |
|
|
|
|
| 19 |
@app.route("/query", methods=["POST"])
|
| 20 |
def query():
|
| 21 |
data = request.get_json()
|
|
@@ -33,6 +35,57 @@ def query():
|
|
| 33 |
except Exception as e:
|
| 34 |
return jsonify({"error": str(e)}), 500
|
| 35 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
@app.route("/export-pdf", methods=["POST"])
|
| 37 |
def export_pdf():
|
| 38 |
data = request.get_json()
|
|
@@ -69,7 +122,7 @@ def export_pdf():
|
|
| 69 |
story = []
|
| 70 |
story.append(Paragraph("ARIA — Autonomous Research Intelligence Agent", title_style))
|
| 71 |
story.append(Paragraph(
|
| 72 |
-
|
| 73 |
meta_style))
|
| 74 |
story.append(HRFlowable(width="100%", thickness=1,
|
| 75 |
color=colors.HexColor("#1e2936"), spaceAfter=16))
|
|
@@ -116,9 +169,10 @@ def export_pdf():
|
|
| 116 |
doc.build(story)
|
| 117 |
buf.seek(0)
|
| 118 |
safe_query = "".join(c for c in query[:40] if c.isalnum() or c in " -_").strip()
|
| 119 |
-
filename =
|
| 120 |
return send_file(buf, mimetype="application/pdf",
|
| 121 |
as_attachment=True, download_name=filename)
|
| 122 |
|
|
|
|
| 123 |
if __name__ == "__main__":
|
| 124 |
-
app.run(debug=True, port=5000)
|
|
|
|
| 1 |
+
import sys, os, json
|
| 2 |
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
| 3 |
+
from flask import Flask, render_template, request, jsonify, send_file, Response, stream_with_context
|
| 4 |
+
from agent.agent import run_pipeline, run_query_architect, run_literature_scout, run_evidence_synthesiser, run_citation_builder
|
| 5 |
from reportlab.lib.pagesizes import A4
|
| 6 |
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
| 7 |
from reportlab.lib.units import mm
|
|
|
|
| 12 |
|
| 13 |
app = Flask(__name__)
|
| 14 |
|
| 15 |
+
|
| 16 |
@app.route("/")
|
| 17 |
def index():
|
| 18 |
return render_template("index.html")
|
| 19 |
|
| 20 |
+
|
| 21 |
@app.route("/query", methods=["POST"])
|
| 22 |
def query():
|
| 23 |
data = request.get_json()
|
|
|
|
| 35 |
except Exception as e:
|
| 36 |
return jsonify({"error": str(e)}), 500
|
| 37 |
|
| 38 |
+
|
| 39 |
+
@app.route("/stream", methods=["GET"])
|
| 40 |
+
def stream():
|
| 41 |
+
user_query = request.args.get("query", "").strip()
|
| 42 |
+
if not user_query:
|
| 43 |
+
return jsonify({"error": "Empty query"}), 400
|
| 44 |
+
|
| 45 |
+
def generate():
|
| 46 |
+
def emit(event, data):
|
| 47 |
+
return "event: " + event + "\ndata: " + json.dumps(data) + "\n\n"
|
| 48 |
+
|
| 49 |
+
try:
|
| 50 |
+
# Stage 1
|
| 51 |
+
yield emit("stage", {"stage": 1, "pct": 10})
|
| 52 |
+
queries = run_query_architect(user_query)
|
| 53 |
+
yield emit("queries", {"queries": queries, "pct": 25})
|
| 54 |
+
|
| 55 |
+
# Stage 2
|
| 56 |
+
yield emit("stage", {"stage": 2, "pct": 35})
|
| 57 |
+
papers = run_literature_scout(queries)
|
| 58 |
+
yield emit("papers", {"paper_count": len(papers), "pct": 55})
|
| 59 |
+
|
| 60 |
+
# Stage 3
|
| 61 |
+
yield emit("stage", {"stage": 3, "pct": 70})
|
| 62 |
+
synthesis = run_evidence_synthesiser(user_query, papers)
|
| 63 |
+
yield emit("synthesis", {"synthesis": synthesis, "pct": 88})
|
| 64 |
+
|
| 65 |
+
# Stage 4
|
| 66 |
+
yield emit("stage", {"stage": 4, "pct": 90})
|
| 67 |
+
citations = run_citation_builder(papers)
|
| 68 |
+
yield emit("done", {
|
| 69 |
+
"synthesis": synthesis,
|
| 70 |
+
"citations": citations,
|
| 71 |
+
"paper_count": len(papers),
|
| 72 |
+
"queries": queries,
|
| 73 |
+
"pct": 100
|
| 74 |
+
})
|
| 75 |
+
|
| 76 |
+
except Exception as e:
|
| 77 |
+
yield emit("error", {"message": str(e)})
|
| 78 |
+
|
| 79 |
+
return Response(
|
| 80 |
+
stream_with_context(generate()),
|
| 81 |
+
mimetype="text/event-stream",
|
| 82 |
+
headers={
|
| 83 |
+
"Cache-Control": "no-cache",
|
| 84 |
+
"X-Accel-Buffering": "no"
|
| 85 |
+
}
|
| 86 |
+
)
|
| 87 |
+
|
| 88 |
+
|
| 89 |
@app.route("/export-pdf", methods=["POST"])
|
| 90 |
def export_pdf():
|
| 91 |
data = request.get_json()
|
|
|
|
| 122 |
story = []
|
| 123 |
story.append(Paragraph("ARIA — Autonomous Research Intelligence Agent", title_style))
|
| 124 |
story.append(Paragraph(
|
| 125 |
+
"Query: " + query + " | " + str(paper_count) + " papers retrieved | Groq LLaMA-3.1",
|
| 126 |
meta_style))
|
| 127 |
story.append(HRFlowable(width="100%", thickness=1,
|
| 128 |
color=colors.HexColor("#1e2936"), spaceAfter=16))
|
|
|
|
| 169 |
doc.build(story)
|
| 170 |
buf.seek(0)
|
| 171 |
safe_query = "".join(c for c in query[:40] if c.isalnum() or c in " -_").strip()
|
| 172 |
+
filename = "ARIA_" + safe_query.replace(" ", "_") + ".pdf"
|
| 173 |
return send_file(buf, mimetype="application/pdf",
|
| 174 |
as_attachment=True, download_name=filename)
|
| 175 |
|
| 176 |
+
|
| 177 |
if __name__ == "__main__":
|
| 178 |
+
app.run(debug=True, port=5000, threaded=True)
|
templates/index.html
CHANGED
|
@@ -502,92 +502,90 @@ function toggleCitations() {
|
|
| 502 |
document.getElementById("citationsList").classList.contains("open") ? "▲" : "▼";
|
| 503 |
}
|
| 504 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 505 |
async function submitQuery() {
|
| 506 |
const q = document.getElementById("queryInput").value.trim();
|
| 507 |
if (!q) return;
|
| 508 |
-
|
| 509 |
const btn = document.getElementById("submitBtn");
|
| 510 |
btn.disabled = true;
|
| 511 |
-
|
| 512 |
document.getElementById("results").classList.remove("visible");
|
| 513 |
document.getElementById("errorBox").classList.remove("visible");
|
| 514 |
document.getElementById("pipeline").classList.add("visible");
|
| 515 |
setStage(1);
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
const
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
|
| 527 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 528 |
lastResult = data;
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
const meta = document.getElementById("metaBar");
|
| 541 |
-
meta.innerHTML = `
|
| 542 |
-
<div class="badge green">${data.paper_count} papers retrieved</div>
|
| 543 |
-
<div class="badge">${data.queries ? data.queries.length : 0} PubMed queries</div>
|
| 544 |
-
<div class="badge">LLaMA-3.1-8B via Groq</div>
|
| 545 |
-
`;
|
| 546 |
-
|
| 547 |
-
// Synthesis sections
|
| 548 |
-
const sections = parseSynthesis(data.synthesis);
|
| 549 |
-
const synthEl = document.getElementById("synthesis");
|
| 550 |
-
synthEl.innerHTML = "";
|
| 551 |
-
SECTIONS.forEach(s => {
|
| 552 |
-
if (!sections[s.label]) return;
|
| 553 |
-
const div = document.createElement("div");
|
| 554 |
-
div.className = "section open";
|
| 555 |
-
div.innerHTML = `
|
| 556 |
-
<div class="section-header" onclick="toggleSection(this.parentElement)">
|
| 557 |
-
<span class="section-tag">${s.label}</span>
|
| 558 |
-
<span class="section-chevron">▲</span>
|
| 559 |
-
</div>
|
| 560 |
-
<div class="section-body">${sections[s.label].replace(/\n/g, '<br>')}</div>
|
| 561 |
-
`;
|
| 562 |
-
synthEl.appendChild(div);
|
| 563 |
-
});
|
| 564 |
-
|
| 565 |
-
// Citations
|
| 566 |
-
const citList = document.getElementById("citationsList");
|
| 567 |
-
citList.innerHTML = "";
|
| 568 |
-
citList.classList.add("open");
|
| 569 |
-
data.citations.split("\n").forEach(line => {
|
| 570 |
-
if (!line.trim()) return;
|
| 571 |
-
const pmidMatch = line.match(/PMID:\s*(\d+)/);
|
| 572 |
-
const pmid = pmidMatch ? pmidMatch[1] : null;
|
| 573 |
-
const div = document.createElement("div");
|
| 574 |
-
div.className = "citation-item";
|
| 575 |
-
div.innerHTML = pmid
|
| 576 |
-
? line.replace(/PMID:\s*\d+/, `<span class="citation-pmid">PMID: ${pmid}</span>`)
|
| 577 |
-
: line;
|
| 578 |
-
citList.appendChild(div);
|
| 579 |
-
});
|
| 580 |
-
|
| 581 |
-
document.getElementById("results").classList.add("visible");
|
| 582 |
-
|
| 583 |
-
} catch (err) {
|
| 584 |
-
document.getElementById("errorBox").textContent = "Network error: " + err.message;
|
| 585 |
document.getElementById("errorBox").classList.add("visible");
|
| 586 |
-
} finally {
|
| 587 |
btn.disabled = false;
|
| 588 |
-
}
|
| 589 |
}
|
| 590 |
|
|
|
|
| 591 |
document.getElementById("queryInput").addEventListener("keydown", e => {
|
| 592 |
if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); submitQuery(); }
|
| 593 |
});
|
|
|
|
| 502 |
document.getElementById("citationsList").classList.contains("open") ? "▲" : "▼";
|
| 503 |
}
|
| 504 |
|
| 505 |
+
function renderResults(data) {
|
| 506 |
+
const meta = document.getElementById("metaBar");
|
| 507 |
+
meta.innerHTML = `<div class="badge green">${data.paper_count} papers retrieved</div><div class="badge">${data.queries ? data.queries.length : 0} PubMed queries</div><div class="badge">LLaMA-3.1-8B via Groq</div>`;
|
| 508 |
+
const sections = parseSynthesis(data.synthesis);
|
| 509 |
+
const synthEl = document.getElementById("synthesis");
|
| 510 |
+
synthEl.innerHTML = "";
|
| 511 |
+
SECTIONS.forEach(s => {
|
| 512 |
+
if (!sections[s.label]) return;
|
| 513 |
+
const div = document.createElement("div");
|
| 514 |
+
div.className = "section open";
|
| 515 |
+
div.innerHTML = `<div class="section-header" onclick="toggleSection(this.parentElement)"><span class="section-tag">${s.label}</span><span class="section-chevron">▲</span></div><div class="section-body">${sections[s.label].replace(/\n/g, "<br>")}</div>`;
|
| 516 |
+
synthEl.appendChild(div);
|
| 517 |
+
});
|
| 518 |
+
const citList = document.getElementById("citationsList");
|
| 519 |
+
citList.innerHTML = "";
|
| 520 |
+
citList.classList.add("open");
|
| 521 |
+
data.citations.split("\n").forEach(line => {
|
| 522 |
+
if (!line.trim()) return;
|
| 523 |
+
const pmidMatch = line.match(/PMID:\s*(\d+)/);
|
| 524 |
+
const pmid = pmidMatch ? pmidMatch[1] : null;
|
| 525 |
+
const div = document.createElement("div");
|
| 526 |
+
div.className = "citation-item";
|
| 527 |
+
div.innerHTML = pmid ? line.replace(/PMID:\s*\d+/, `<span class="citation-pmid">PMID: ${pmid}</span>`) : line;
|
| 528 |
+
citList.appendChild(div);
|
| 529 |
+
});
|
| 530 |
+
document.getElementById("results").classList.add("visible");
|
| 531 |
+
}
|
| 532 |
+
|
| 533 |
+
function updateProgress(pct) {
|
| 534 |
+
document.getElementById("progressFill").style.width = pct + "%";
|
| 535 |
+
document.getElementById("progressPct").textContent = pct + "%";
|
| 536 |
+
}
|
| 537 |
+
|
| 538 |
async function submitQuery() {
|
| 539 |
const q = document.getElementById("queryInput").value.trim();
|
| 540 |
if (!q) return;
|
|
|
|
| 541 |
const btn = document.getElementById("submitBtn");
|
| 542 |
btn.disabled = true;
|
|
|
|
| 543 |
document.getElementById("results").classList.remove("visible");
|
| 544 |
document.getElementById("errorBox").classList.remove("visible");
|
| 545 |
document.getElementById("pipeline").classList.add("visible");
|
| 546 |
setStage(1);
|
| 547 |
+
updateProgress(5);
|
| 548 |
+
lastQuery = q;
|
| 549 |
+
|
| 550 |
+
const es = new EventSource("/stream?query=" + encodeURIComponent(q));
|
| 551 |
+
|
| 552 |
+
es.addEventListener("stage", e => {
|
| 553 |
+
const d = JSON.parse(e.data);
|
| 554 |
+
setStage(d.stage);
|
| 555 |
+
updateProgress(d.pct);
|
| 556 |
+
});
|
| 557 |
+
es.addEventListener("queries", e => {
|
| 558 |
+
const d = JSON.parse(e.data);
|
| 559 |
+
updateProgress(d.pct);
|
| 560 |
+
});
|
| 561 |
+
es.addEventListener("papers", e => {
|
| 562 |
+
const d = JSON.parse(e.data);
|
| 563 |
+
updateProgress(d.pct);
|
| 564 |
+
});
|
| 565 |
+
es.addEventListener("synthesis", e => {
|
| 566 |
+
const d = JSON.parse(e.data);
|
| 567 |
+
updateProgress(d.pct);
|
| 568 |
+
});
|
| 569 |
+
es.addEventListener("done", e => {
|
| 570 |
+
const data = JSON.parse(e.data);
|
| 571 |
lastResult = data;
|
| 572 |
+
updateProgress(100);
|
| 573 |
+
setStage(5);
|
| 574 |
+
es.close();
|
| 575 |
+
renderResults(data);
|
| 576 |
+
btn.disabled = false;
|
| 577 |
+
});
|
| 578 |
+
es.addEventListener("error", e => {
|
| 579 |
+
es.close();
|
| 580 |
+
let msg = "Pipeline error";
|
| 581 |
+
try { msg = JSON.parse(e.data).message; } catch(err) {}
|
| 582 |
+
document.getElementById("errorBox").textContent = "Error: " + msg;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 583 |
document.getElementById("errorBox").classList.add("visible");
|
|
|
|
| 584 |
btn.disabled = false;
|
| 585 |
+
});
|
| 586 |
}
|
| 587 |
|
| 588 |
+
|
| 589 |
document.getElementById("queryInput").addEventListener("keydown", e => {
|
| 590 |
if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); submitQuery(); }
|
| 591 |
});
|