deepamr-api / src /api /reports.py
hossainlab's picture
Deploy DeepAMR API backend
3255634
"""PDF report generation for DeepAMR predictions."""
import io
from datetime import datetime
from typing import Dict, List, Optional
from reportlab.lib import colors
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import mm
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, HRFlowable,
)
def generate_prediction_report(prediction: Dict) -> bytes:
"""Generate a PDF clinical report for an AMR prediction.
Args:
prediction: Prediction dict from the database (frontend format).
Returns:
PDF file content as bytes.
"""
buf = io.BytesIO()
doc = SimpleDocTemplate(buf, pagesize=A4, topMargin=20 * mm, bottomMargin=20 * mm)
styles = getSampleStyleSheet()
title_style = ParagraphStyle("ReportTitle", parent=styles["Title"], fontSize=18, spaceAfter=6)
subtitle_style = ParagraphStyle("Sub", parent=styles["Normal"], fontSize=10, textColor=colors.grey)
section_style = ParagraphStyle("Section", parent=styles["Heading2"], fontSize=13, spaceBefore=14)
body_style = styles["Normal"]
disclaimer_style = ParagraphStyle("Disclaimer", parent=styles["Normal"], fontSize=8, textColor=colors.grey)
elements: list = []
# Title
elements.append(Paragraph("DeepAMR - Antimicrobial Resistance Report", title_style))
elements.append(Paragraph(f"Generated: {datetime.utcnow().strftime('%Y-%m-%d %H:%M UTC')}", subtitle_style))
elements.append(HRFlowable(width="100%", thickness=1, color=colors.grey))
elements.append(Spacer(1, 6 * mm))
# Sample info
elements.append(Paragraph("Sample Information", section_style))
sample_data = [
["Sample ID", prediction.get("sampleId", "N/A")],
["Organism", prediction.get("organism", "Unknown")],
["File", prediction.get("fileName", "N/A")],
["Date", prediction.get("createdAt", "N/A")],
["Overall Risk", (prediction.get("overallRisk") or "N/A").upper()],
]
if prediction.get("model_version"):
sample_data.append(["Model Version", prediction["model_version"]])
t = Table(sample_data, colWidths=[120, 350])
t.setStyle(TableStyle([
("FONTNAME", (0, 0), (0, -1), "Helvetica-Bold"),
("FONTSIZE", (0, 0), (-1, -1), 10),
("BOTTOMPADDING", (0, 0), (-1, -1), 4),
]))
elements.append(t)
elements.append(Spacer(1, 6 * mm))
# Drug resistance table
results = prediction.get("results") or []
if results:
elements.append(Paragraph("Drug Resistance Profile", section_style))
header = ["Antibiotic", "Status", "Confidence"]
rows = [header]
for r in results:
status = r.get("status", "?")
conf = r.get("confidence", 0)
rows.append([
r.get("antibiotic", r.get("class", "?")),
"Resistant" if status == "R" else "Susceptible",
f"{conf * 100:.1f}%",
])
drug_table = Table(rows, colWidths=[200, 120, 100])
# Color-code status column
style_cmds = [
("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#1e40af")),
("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
("FONTSIZE", (0, 0), (-1, -1), 9),
("GRID", (0, 0), (-1, -1), 0.5, colors.grey),
("BOTTOMPADDING", (0, 0), (-1, -1), 4),
("TOPPADDING", (0, 0), (-1, -1), 4),
]
for i, r in enumerate(results, start=1):
if r.get("status") == "R":
style_cmds.append(("BACKGROUND", (1, i), (1, i), colors.HexColor("#fee2e2")))
style_cmds.append(("TEXTCOLOR", (1, i), (1, i), colors.HexColor("#dc2626")))
else:
style_cmds.append(("BACKGROUND", (1, i), (1, i), colors.HexColor("#dcfce7")))
style_cmds.append(("TEXTCOLOR", (1, i), (1, i), colors.HexColor("#16a34a")))
drug_table.setStyle(TableStyle(style_cmds))
elements.append(drug_table)
elements.append(Spacer(1, 6 * mm))
# Summary
summary = prediction.get("summary")
if summary:
elements.append(Paragraph("Summary", section_style))
elements.append(Paragraph(
f"Resistant: {summary.get('resistant', 0)} | "
f"Intermediate: {summary.get('intermediate', 0)} | "
f"Susceptible: {summary.get('susceptible', 0)}",
body_style,
))
elements.append(Spacer(1, 4 * mm))
# Recommendations (if stored)
recs = prediction.get("recommendations")
if recs:
elements.append(Paragraph("Clinical Recommendations", section_style))
for rec in recs:
elements.append(Paragraph(f"• {rec}", body_style))
elements.append(Spacer(1, 4 * mm))
# Bangladesh context (if stored)
bd_recs = prediction.get("bangladesh_recommendations")
if bd_recs:
elements.append(Paragraph("Bangladesh Clinical Context", section_style))
for rec in bd_recs:
elements.append(Paragraph(f"• {rec}", body_style))
elements.append(Spacer(1, 4 * mm))
# Disclaimer
elements.append(Spacer(1, 10 * mm))
elements.append(HRFlowable(width="100%", thickness=0.5, color=colors.grey))
elements.append(Paragraph(
"DISCLAIMER: This report is generated by an AI model (DeepAMR Advanced DL, "
"Micro F1: 84.3%, AUC: 98.6%). Results are intended to assist clinical decision-making "
"and should NOT replace laboratory-confirmed susceptibility testing. "
"Always consult qualified healthcare professionals before making treatment decisions.",
disclaimer_style,
))
doc.build(elements)
return buf.getvalue()