| import sys, os, json, time |
| sys.path.append(os.path.dirname(os.path.abspath(__file__))) |
| from flask import Flask, render_template, request, jsonify, send_file, Response, stream_with_context |
| from agent.agent import (run_pipeline, run_query_architect, run_literature_scout, |
| run_evidence_synthesiser, run_citation_builder, llm_invoke_with_retry, get_llm) |
| from reportlab.lib.pagesizes import A4 |
| from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle |
| from reportlab.lib.units import mm |
| from reportlab.lib import colors |
| from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, HRFlowable |
| from reportlab.lib.enums import TA_LEFT |
| import io |
|
|
| app = Flask(__name__) |
|
|
| @app.route("/") |
| def index(): |
| return render_template("index.html") |
|
|
| @app.route("/query", methods=["POST"]) |
| def query(): |
| data = request.get_json() |
| user_query = data.get("query", "").strip() |
| if not user_query: |
| return jsonify({"error": "Empty query"}), 400 |
| try: |
| result = run_pipeline(user_query) |
| return jsonify({ |
| "synthesis": result["synthesis"], |
| "citations": result["citations"], |
| "paper_count": result["paper_count"], |
| "queries": result["queries"] |
| }) |
| except Exception as e: |
| return jsonify({"error": str(e)}), 500 |
|
|
| @app.route("/stream", methods=["GET"]) |
| def stream(): |
| user_query = request.args.get("query", "").strip() |
| if not user_query: |
| return jsonify({"error": "Empty query"}), 400 |
|
|
| def generate(): |
| def emit(event, data): |
| return "event: " + event + "\ndata: " + json.dumps(data) + "\n\n" |
|
|
| try: |
| |
| yield emit("stage", {"stage": 1, "pct": 10}) |
| queries = run_query_architect(user_query) |
| yield emit("queries", {"queries": queries, "pct": 25}) |
|
|
| |
| yield emit("stage", {"stage": 2, "pct": 35}) |
| papers = run_literature_scout(queries) |
| yield emit("papers", {"paper_count": len(papers), "pct": 50}) |
|
|
| |
| yield emit("stage", {"stage": 3, "pct": 55}) |
| from agent.agent import run_prisma_filter |
| filtered = run_prisma_filter(user_query, papers) |
| included = {pmid: p for pmid, p in filtered.items() if p["included"]} |
| yield emit("prisma", { |
| "filtered": { |
| pmid: {"title": p.get("title", ""), "included": p["included"], "reason": p["reason"]} |
| for pmid, p in filtered.items() |
| }, |
| "included_count": len(included), |
| "excluded_count": len(filtered) - len(included), |
| "pct": 65 |
| }) |
| time.sleep(12) |
|
|
| |
| yield emit("stage", {"stage": 4, "pct": 70}) |
| synthesis = run_evidence_synthesiser(user_query, included) |
| yield emit("synthesis", {"synthesis": synthesis, "pct": 88}) |
|
|
| |
| yield emit("stage", {"stage": 5, "pct": 90}) |
| citations = run_citation_builder(included) |
| yield emit("done", { |
| "synthesis": synthesis, |
| "citations": citations, |
| "paper_count": len(included), |
| "queries": queries, |
| "papers": { |
| pmid: { |
| "title": p.get("title", ""), |
| "abstract": p.get("abstract", ""), |
| "authors": p.get("authors", ""), |
| "journal": p.get("journal", ""), |
| "year": p.get("year", "") |
| } for pmid, p in included.items() |
| }, |
| "pct": 100 |
| }) |
|
|
| except Exception as e: |
| yield emit("error", {"message": str(e)}) |
|
|
| return Response( |
| stream_with_context(generate()), |
| mimetype="text/event-stream", |
| headers={ |
| "Cache-Control": "no-cache", |
| "X-Accel-Buffering": "no" |
| } |
| ) |
|
|
| @app.route("/suggest-queries", methods=["POST"]) |
| def suggest_queries(): |
| data = request.get_json() |
| original_query = data.get("query", "") |
| synthesis = data.get("synthesis", "") |
| if not synthesis: |
| return jsonify({"error": "No synthesis provided"}), 400 |
| try: |
| llm = get_llm() |
| prompt = ( |
| f"You are a biomedical research strategist. A researcher asked:\n\"{original_query}\"\n\n" |
| f"Based on this evidence synthesis, identify 3 high-value follow-up research questions " |
| f"that would fill gaps or extend the findings. Return ONLY a JSON array of 3 strings, " |
| f"each a specific, searchable research question. No preamble, no markdown, just the JSON array.\n\n" |
| f"Synthesis excerpt:\n{synthesis[:1200]}" |
| ) |
| response = llm_invoke_with_retry(llm, prompt) |
| raw = response.content.strip() |
| |
| if raw.startswith("```"): |
| raw = raw.split("```")[1] |
| if raw.startswith("json"): |
| raw = raw[4:] |
| suggestions = json.loads(raw.strip()) |
| if not isinstance(suggestions, list): |
| suggestions = [] |
| return jsonify({"suggestions": suggestions[:3]}) |
| except Exception as e: |
| return jsonify({"error": str(e)}), 500 |
|
|
| @app.route("/export-pdf", methods=["POST"]) |
| def export_pdf(): |
| data = request.get_json() |
| synthesis = data.get("synthesis", "") |
| citations = data.get("citations", "") |
| query = data.get("query", "Biomedical Research Query") |
| paper_count = data.get("paper_count", 0) |
|
|
| buf = io.BytesIO() |
| doc = SimpleDocTemplate(buf, pagesize=A4, |
| leftMargin=20*mm, rightMargin=20*mm, |
| topMargin=20*mm, bottomMargin=20*mm) |
|
|
| accent = colors.HexColor("#00e5a0") |
| dark = colors.HexColor("#111827") |
|
|
| title_style = ParagraphStyle("title", |
| fontName="Helvetica-Bold", fontSize=18, |
| textColor=dark, spaceAfter=4) |
| meta_style = ParagraphStyle("meta", |
| fontName="Helvetica", fontSize=9, |
| textColor=colors.HexColor("#5a6a7a"), spaceAfter=16) |
| section_label_style = ParagraphStyle("sec_label", |
| fontName="Helvetica-Bold", fontSize=10, |
| textColor=accent, spaceBefore=14, spaceAfter=4) |
| body_style = ParagraphStyle("body", |
| fontName="Helvetica", fontSize=10, |
| leading=16, textColor=dark, spaceAfter=6) |
| cite_style = ParagraphStyle("cite", |
| fontName="Helvetica", fontSize=8, |
| leading=13, textColor=colors.HexColor("#444444"), |
| spaceAfter=4) |
|
|
| story = [] |
| story.append(Paragraph("ARIA — Autonomous Research Intelligence Agent", title_style)) |
| story.append(Paragraph( |
| "Query: " + query + " | " + str(paper_count) + " papers retrieved | Groq LLaMA-3.1", |
| meta_style)) |
| story.append(HRFlowable(width="100%", thickness=1, |
| color=colors.HexColor("#1e2936"), spaceAfter=16)) |
|
|
| SECTIONS = [ |
| ("## Background", "Background"), |
| ("## Key Findings", "Key Findings"), |
| ("## Level of Evidence", "Level of Evidence"), |
| ("## Conflicting Evidence", "Conflicting Evidence"), |
| ("## Research Gaps", "Research Gaps"), |
| ("## Clinical Implications", "Clinical Implications"), |
| ] |
| for marker, label in SECTIONS: |
| start = synthesis.find(marker) |
| if start == -1: |
| continue |
| content_start = start + len(marker) |
| next_markers = [synthesis.find(m) for m, _ in SECTIONS if synthesis.find(m) > start] |
| end = min(next_markers) if next_markers else len(synthesis) |
| text = synthesis[content_start:end].strip() |
| if not text: |
| continue |
| story.append(Paragraph(label.upper(), section_label_style)) |
| for para in text.split("\n"): |
| para = para.strip() |
| if para: |
| story.append(Paragraph(para, body_style)) |
|
|
| story.append(Spacer(1, 8*mm)) |
| story.append(HRFlowable(width="100%", thickness=1, |
| color=colors.HexColor("#1e2936"), spaceAfter=8)) |
| story.append(Paragraph("REFERENCES", section_label_style)) |
| for line in citations.split("\n"): |
| line = line.strip() |
| if line: |
| story.append(Paragraph(line, cite_style)) |
|
|
| story.append(Spacer(1, 6*mm)) |
| story.append(Paragraph( |
| "AI-generated synthesis — verify against primary sources before clinical use.", |
| ParagraphStyle("disclaimer", fontName="Helvetica-Oblique", |
| fontSize=8, textColor=colors.HexColor("#999999")))) |
|
|
| doc.build(story) |
| buf.seek(0) |
| safe_query = "".join(c for c in query[:40] if c.isalnum() or c in " -_").strip() |
| filename = "ARIA_" + safe_query.replace(" ", "_") + ".pdf" |
| return send_file(buf, mimetype="application/pdf", |
| as_attachment=True, download_name=filename) |
|
|
| @app.route("/score", methods=["POST"]) |
| def score(): |
| data = request.get_json() |
| synthesis = data.get("synthesis", "") |
| if not synthesis: |
| return jsonify({"error": "No synthesis provided"}), 400 |
| try: |
| from agent.agent import run_confidence_scorer |
| scores = run_confidence_scorer(synthesis) |
| return jsonify({"scores": scores}) |
| except Exception as e: |
| return jsonify({"error": str(e)}), 500 |
|
|
| @app.route("/selective-review", methods=["POST"]) |
| def selective_review(): |
| data = request.get_json() |
| question = data.get("question", "") |
| selected_papers = data.get("papers", {}) |
| if not selected_papers: |
| return jsonify({"error": "No papers selected"}), 400 |
| try: |
| from agent.agent import run_selective_review |
| review = run_selective_review(question, selected_papers) |
| return jsonify({"review": review}) |
| except Exception as e: |
| return jsonify({"error": str(e)}), 500 |
|
|
| @app.route("/predict", methods=["POST"]) |
| def predict(): |
| data = request.get_json() |
| question = data.get("question", "") |
| synthesis = data.get("synthesis", "") |
| if not synthesis: |
| return jsonify({"error": "No synthesis provided"}), 400 |
| try: |
| from agent.agent import run_predictive_model |
| prediction = run_predictive_model(question, synthesis) |
| return jsonify({"prediction": prediction}) |
| except Exception as e: |
| return jsonify({"error": str(e)}), 500 |
|
|
| import json as _json |
| from datetime import datetime |
| SESSIONS_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "sessions.json") |
|
|
| def load_sessions(): |
| try: |
| return _json.load(open(SESSIONS_FILE)) |
| except: |
| return [] |
|
|
| def save_session(entry): |
| sessions = load_sessions() |
| sessions.insert(0, entry) |
| sessions = sessions[:20] |
| _json.dump(sessions, open(SESSIONS_FILE, "w"), indent=2) |
|
|
| @app.route("/sessions", methods=["GET"]) |
| def get_sessions(): |
| return jsonify({"sessions": load_sessions()}) |
|
|
| @app.route("/sessions/save", methods=["POST"]) |
| def save_session_route(): |
| data = request.get_json() |
| save_session({ |
| "id": datetime.now().strftime("%Y%m%d%H%M%S"), |
| "timestamp": datetime.now().strftime("%b %d, %H:%M"), |
| "query": data.get("query", ""), |
| "synthesis": data.get("synthesis", ""), |
| "citations": data.get("citations", ""), |
| "paper_count": data.get("paper_count", 0), |
| "queries": data.get("queries", []), |
| "papers": data.get("papers", {}) |
| }) |
| return jsonify({"ok": True}) |
|
|
| @app.route("/extract-table", methods=["POST"]) |
| def extract_table(): |
| data = request.get_json() |
| question = data.get("question", "") |
| synthesis = data.get("synthesis", "") |
| papers = data.get("papers", {}) |
| if not synthesis: |
| return jsonify({"error": "No synthesis provided"}), 400 |
| try: |
| from agent.agent import run_table_extractor |
| table = run_table_extractor(question, synthesis, papers) |
| return jsonify({"table": table}) |
| except Exception as e: |
| import traceback |
| traceback.print_exc() |
| return jsonify({"error": str(e)}), 500 |
|
|
| @app.route("/followup", methods=["POST"]) |
| def followup(): |
| data = request.get_json() |
| question = data.get("question", "") |
| original_question = data.get("original_question", "") |
| synthesis = data.get("synthesis", "") |
| papers = data.get("papers", {}) |
| if not question or not synthesis: |
| return jsonify({"error": "Missing question or synthesis"}), 400 |
| try: |
| llm = get_llm() |
| corpus = "\n\n".join( |
| f"[PMID {pmid}] {p.get('title','')}\n{p.get('abstract','')[:300]}" |
| for pmid, p in list(papers.items())[:6] |
| ) |
| prompt = ( |
| f"You are a biomedical research assistant. The user previously asked:\n" |
| f"\"{original_question}\"\n\n" |
| f"Based on this evidence synthesis and retrieved papers, answer their follow-up question.\n" |
| f"Be concise and cite PMIDs where relevant.\n\n" |
| f"Synthesis:\n{synthesis[:1500]}\n\n" |
| f"Papers:\n{corpus}\n\n" |
| f"Follow-up Question: {question}" |
| ) |
| response = llm_invoke_with_retry(llm, prompt) |
| return jsonify({"answer": response.content}) |
| except Exception as e: |
| return jsonify({"error": str(e)}), 500 |
|
|
| if __name__ == "__main__": |
| port = int(os.environ.get("PORT", 7860)) |
| app.run(host="0.0.0.0", port=port, debug=False, threaded=True) |