azlaan428 commited on
Commit ·
9083f85
1
Parent(s): a7f8aab
feat: selective literature review with paper checkboxes
Browse files- agent/agent.py +23 -0
- app.py +15 -0
- templates/index.html +130 -6
agent/agent.py
CHANGED
|
@@ -128,6 +128,29 @@ def run_confidence_scorer(synthesis):
|
|
| 128 |
text = text.replace("```json", "").replace("```", "").strip()
|
| 129 |
return json.loads(text)
|
| 130 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
def run_pipeline(user_question):
|
| 132 |
print("[1/4] Query Architect: generating search queries...")
|
| 133 |
queries = run_query_architect(user_question)
|
|
|
|
| 128 |
text = text.replace("```json", "").replace("```", "").strip()
|
| 129 |
return json.loads(text)
|
| 130 |
|
| 131 |
+
|
| 132 |
+
def run_selective_review(user_question, selected_papers):
|
| 133 |
+
llm = get_llm()
|
| 134 |
+
parts = []
|
| 135 |
+
for pmid, p in selected_papers.items():
|
| 136 |
+
title = p.get("title", "N/A")
|
| 137 |
+
abstract = p.get("abstract", "")[:400]
|
| 138 |
+
authors = p.get("authors", "")
|
| 139 |
+
year = p.get("year", "")
|
| 140 |
+
parts.append("[PMID " + pmid + "] " + authors + " (" + year + "). " + title + "\n" + abstract)
|
| 141 |
+
corpus = "\n\n".join(parts)
|
| 142 |
+
prompt = (
|
| 143 |
+
"You are an academic writer producing a literature review paragraph.\n"
|
| 144 |
+
"Write a single cohesive academic paragraph (200-300 words) that synthesises "
|
| 145 |
+
"the following selected papers in relation to this question.\n"
|
| 146 |
+
"Cite papers inline by PMID in parentheses e.g. (PMID: 12345678).\n"
|
| 147 |
+
"Write in formal academic prose. No bullet points. No headings.\n\n"
|
| 148 |
+
"Question: " + user_question + "\n\n"
|
| 149 |
+
"Selected Papers:\n" + corpus
|
| 150 |
+
)
|
| 151 |
+
response = llm.invoke(prompt)
|
| 152 |
+
return response.content
|
| 153 |
+
|
| 154 |
def run_pipeline(user_question):
|
| 155 |
print("[1/4] Query Architect: generating search queries...")
|
| 156 |
queries = run_query_architect(user_question)
|
app.py
CHANGED
|
@@ -189,5 +189,20 @@ def score():
|
|
| 189 |
except Exception as e:
|
| 190 |
return jsonify({"error": str(e)}), 500
|
| 191 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 192 |
if __name__ == "__main__":
|
| 193 |
app.run(debug=True, port=5000, threaded=True)
|
|
|
|
| 189 |
except Exception as e:
|
| 190 |
return jsonify({"error": str(e)}), 500
|
| 191 |
|
| 192 |
+
|
| 193 |
+
@app.route("/selective-review", methods=["POST"])
|
| 194 |
+
def selective_review():
|
| 195 |
+
data = request.get_json()
|
| 196 |
+
question = data.get("question", "")
|
| 197 |
+
selected_papers = data.get("papers", {})
|
| 198 |
+
if not selected_papers:
|
| 199 |
+
return jsonify({"error": "No papers selected"}), 400
|
| 200 |
+
try:
|
| 201 |
+
from agent.agent import run_selective_review
|
| 202 |
+
review = run_selective_review(question, selected_papers)
|
| 203 |
+
return jsonify({"review": review})
|
| 204 |
+
except Exception as e:
|
| 205 |
+
return jsonify({"error": str(e)}), 500
|
| 206 |
+
|
| 207 |
if __name__ == "__main__":
|
| 208 |
app.run(debug=True, port=5000, threaded=True)
|
templates/index.html
CHANGED
|
@@ -459,6 +459,66 @@
|
|
| 459 |
font-size: 10px;
|
| 460 |
margin-left: 6px;
|
| 461 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 462 |
</style>
|
| 463 |
</head>
|
| 464 |
<body>
|
|
@@ -512,6 +572,14 @@
|
|
| 512 |
<div class="citations-list" id="citationsList"></div>
|
| 513 |
</div>
|
| 514 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 515 |
|
| 516 |
<footer>
|
| 517 |
<span>Powered by LangGraph · Groq LLaMA-3.1 · PubMed NCBI</span>
|
|
@@ -584,22 +652,39 @@ function renderResults(data) {
|
|
| 584 |
citList.innerHTML = "";
|
| 585 |
citList.classList.add("open");
|
| 586 |
const papersMap = data.papers || {};
|
|
|
|
|
|
|
| 587 |
data.citations.split("\n").forEach(line => {
|
| 588 |
if (!line.trim()) return;
|
| 589 |
const pmidMatch = line.match(/PMID:\s*(\d+)/);
|
| 590 |
const pmid = pmidMatch ? pmidMatch[1] : null;
|
|
|
|
| 591 |
const div = document.createElement("div");
|
| 592 |
div.className = "citation-item";
|
| 593 |
-
const paper = pmid ? papersMap[pmid] : null;
|
| 594 |
const citText = pmid ? line.replace(/PMID:\s*\d+/, `<span class="citation-pmid">PMID: ${pmid}</span>`) : line;
|
| 595 |
-
|
|
|
|
|
|
|
| 596 |
if (paper) {
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
|
|
|
|
| 601 |
});
|
| 602 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 603 |
citList.appendChild(div);
|
| 604 |
});
|
| 605 |
document.getElementById("results").classList.add("visible");
|
|
@@ -721,6 +806,45 @@ async function scoreResults(synthesis) {
|
|
| 721 |
});
|
| 722 |
} catch(e) { console.warn('Scoring failed:', e); }
|
| 723 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 724 |
</script>
|
| 725 |
</body>
|
| 726 |
</html>
|
|
|
|
| 459 |
font-size: 10px;
|
| 460 |
margin-left: 6px;
|
| 461 |
}
|
| 462 |
+
|
| 463 |
+
.citation-checkbox {
|
| 464 |
+
width: 14px;
|
| 465 |
+
height: 14px;
|
| 466 |
+
margin-right: 8px;
|
| 467 |
+
accent-color: var(--accent);
|
| 468 |
+
cursor: pointer;
|
| 469 |
+
flex-shrink: 0;
|
| 470 |
+
}
|
| 471 |
+
.citation-item { display: flex; align-items: flex-start; gap: 4px; }
|
| 472 |
+
.citation-text { flex: 1; }
|
| 473 |
+
.review-bar {
|
| 474 |
+
display: none;
|
| 475 |
+
align-items: center;
|
| 476 |
+
justify-content: space-between;
|
| 477 |
+
padding: 10px 16px;
|
| 478 |
+
background: var(--accent-dim);
|
| 479 |
+
border: 1px solid var(--accent);
|
| 480 |
+
border-radius: 4px;
|
| 481 |
+
margin-top: 12px;
|
| 482 |
+
font-family: var(--mono);
|
| 483 |
+
font-size: 12px;
|
| 484 |
+
color: var(--accent);
|
| 485 |
+
}
|
| 486 |
+
.review-bar.visible { display: flex; }
|
| 487 |
+
.review-btn {
|
| 488 |
+
background: var(--accent);
|
| 489 |
+
color: #000;
|
| 490 |
+
border: none;
|
| 491 |
+
padding: 6px 16px;
|
| 492 |
+
border-radius: 3px;
|
| 493 |
+
font-family: var(--mono);
|
| 494 |
+
font-size: 11px;
|
| 495 |
+
font-weight: 600;
|
| 496 |
+
cursor: pointer;
|
| 497 |
+
}
|
| 498 |
+
.review-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
| 499 |
+
.review-output {
|
| 500 |
+
display: none;
|
| 501 |
+
margin-top: 16px;
|
| 502 |
+
border: 1px solid var(--accent);
|
| 503 |
+
border-radius: 4px;
|
| 504 |
+
overflow: hidden;
|
| 505 |
+
}
|
| 506 |
+
.review-output.visible { display: block; }
|
| 507 |
+
.review-output-header {
|
| 508 |
+
background: var(--accent-dim);
|
| 509 |
+
padding: 10px 16px;
|
| 510 |
+
font-family: var(--mono);
|
| 511 |
+
font-size: 11px;
|
| 512 |
+
color: var(--accent);
|
| 513 |
+
letter-spacing: 0.1em;
|
| 514 |
+
text-transform: uppercase;
|
| 515 |
+
}
|
| 516 |
+
.review-output-body {
|
| 517 |
+
padding: 16px;
|
| 518 |
+
font-size: 14px;
|
| 519 |
+
line-height: 1.8;
|
| 520 |
+
color: var(--text);
|
| 521 |
+
}
|
| 522 |
</style>
|
| 523 |
</head>
|
| 524 |
<body>
|
|
|
|
| 572 |
<div class="citations-list" id="citationsList"></div>
|
| 573 |
</div>
|
| 574 |
</div>
|
| 575 |
+
<div class="review-bar" id="reviewBar">
|
| 576 |
+
<span id="reviewBarText">0 papers selected</span>
|
| 577 |
+
<button class="review-btn" id="reviewBtn" onclick="generateReview()" disabled>Generate Literature Review</button>
|
| 578 |
+
</div>
|
| 579 |
+
<div class="review-output" id="reviewOutput">
|
| 580 |
+
<div class="review-output-header">Generated Literature Review</div>
|
| 581 |
+
<div class="review-output-body" id="reviewText"></div>
|
| 582 |
+
</div>
|
| 583 |
|
| 584 |
<footer>
|
| 585 |
<span>Powered by LangGraph · Groq LLaMA-3.1 · PubMed NCBI</span>
|
|
|
|
| 652 |
citList.innerHTML = "";
|
| 653 |
citList.classList.add("open");
|
| 654 |
const papersMap = data.papers || {};
|
| 655 |
+
selectedPapers = {};
|
| 656 |
+
updateReviewBar();
|
| 657 |
data.citations.split("\n").forEach(line => {
|
| 658 |
if (!line.trim()) return;
|
| 659 |
const pmidMatch = line.match(/PMID:\s*(\d+)/);
|
| 660 |
const pmid = pmidMatch ? pmidMatch[1] : null;
|
| 661 |
+
const paper = pmid ? papersMap[pmid] : null;
|
| 662 |
const div = document.createElement("div");
|
| 663 |
div.className = "citation-item";
|
|
|
|
| 664 |
const citText = pmid ? line.replace(/PMID:\s*\d+/, `<span class="citation-pmid">PMID: ${pmid}</span>`) : line;
|
| 665 |
+
const inner = document.createElement("div");
|
| 666 |
+
inner.className = "citation-text";
|
| 667 |
+
inner.innerHTML = citText + (paper ? `<span class="citation-toggle">▶ abstract</span><div class="abstract-body">${paper.abstract ? paper.abstract.replace(/\n/g,"<br>") : "Abstract not available"}</div>` : "");
|
| 668 |
if (paper) {
|
| 669 |
+
inner.addEventListener("click", (e) => {
|
| 670 |
+
if (e.target.type === "checkbox") return;
|
| 671 |
+
inner.parentElement.classList.toggle("expanded");
|
| 672 |
+
const tog = inner.querySelector(".citation-toggle");
|
| 673 |
+
if (tog) tog.textContent = inner.parentElement.classList.contains("expanded") ? "▼ abstract" : "▶ abstract";
|
| 674 |
});
|
| 675 |
}
|
| 676 |
+
if (pmid && paper) {
|
| 677 |
+
const cb = document.createElement("input");
|
| 678 |
+
cb.type = "checkbox";
|
| 679 |
+
cb.className = "citation-checkbox";
|
| 680 |
+
cb.addEventListener("change", () => {
|
| 681 |
+
if (cb.checked) selectedPapers[pmid] = paper;
|
| 682 |
+
else delete selectedPapers[pmid];
|
| 683 |
+
updateReviewBar();
|
| 684 |
+
});
|
| 685 |
+
div.appendChild(cb);
|
| 686 |
+
}
|
| 687 |
+
div.appendChild(inner);
|
| 688 |
citList.appendChild(div);
|
| 689 |
});
|
| 690 |
document.getElementById("results").classList.add("visible");
|
|
|
|
| 806 |
});
|
| 807 |
} catch(e) { console.warn('Scoring failed:', e); }
|
| 808 |
}
|
| 809 |
+
|
| 810 |
+
let selectedPapers = {};
|
| 811 |
+
|
| 812 |
+
function updateReviewBar() {
|
| 813 |
+
const count = Object.keys(selectedPapers).length;
|
| 814 |
+
const bar = document.getElementById('reviewBar');
|
| 815 |
+
const btn = document.getElementById('reviewBtn');
|
| 816 |
+
const txt = document.getElementById('reviewBarText');
|
| 817 |
+
bar.classList.toggle('visible', count > 0);
|
| 818 |
+
txt.textContent = count + ' paper' + (count !== 1 ? 's' : '') + ' selected';
|
| 819 |
+
btn.disabled = count < 2;
|
| 820 |
+
}
|
| 821 |
+
|
| 822 |
+
async function generateReview() {
|
| 823 |
+
const btn = document.getElementById('reviewBtn');
|
| 824 |
+
btn.disabled = true;
|
| 825 |
+
btn.textContent = 'Generating...';
|
| 826 |
+
try {
|
| 827 |
+
const res = await fetch('/selective-review', {
|
| 828 |
+
method: 'POST',
|
| 829 |
+
headers: {'Content-Type': 'application/json'},
|
| 830 |
+
body: JSON.stringify({
|
| 831 |
+
question: lastQuery,
|
| 832 |
+
papers: selectedPapers
|
| 833 |
+
})
|
| 834 |
+
});
|
| 835 |
+
const data = await res.json();
|
| 836 |
+
if (data.error) throw new Error(data.error);
|
| 837 |
+
document.getElementById('reviewText').textContent = data.review;
|
| 838 |
+
document.getElementById('reviewOutput').classList.add('visible');
|
| 839 |
+
document.getElementById('reviewOutput').scrollIntoView({behavior: 'smooth'});
|
| 840 |
+
} catch(e) {
|
| 841 |
+
alert('Error: ' + e.message);
|
| 842 |
+
} finally {
|
| 843 |
+
btn.disabled = false;
|
| 844 |
+
btn.textContent = 'Generate Literature Review';
|
| 845 |
+
updateReviewBar();
|
| 846 |
+
}
|
| 847 |
+
}
|
| 848 |
</script>
|
| 849 |
</body>
|
| 850 |
</html>
|