"""
Synthetic clinical guideline PDF generator for OncoAgent.
Creates structured PDFs that mimic NCCN/ESMO guideline format
for testing the RAG ingestion pipeline.
Uses reportlab for PDF generation. All content is synthetic
and for demonstration/testing purposes only.
"""
import os
from typing import List, Dict
# Synthetic guideline content organized by cancer type
SYNTHETIC_GUIDELINES: Dict[str, List[Dict[str, str]]] = {
"lung_cancer": [
{
"header": "Diagnóstico",
"content": (
"La evaluación inicial del paciente con sospecha de cáncer de pulmón "
"debe incluir:\n"
"- Historia clínica completa con énfasis en factores de riesgo "
"(tabaquismo, exposición ocupacional, historia familiar)\n"
"- Exploración física completa\n"
"- TC de tórax con contraste\n"
"- Citología de esputo en pacientes con lesiones centrales\n"
"- Broncoscopia con biopsia para lesiones centrales\n"
"- Biopsia guiada por TC para lesiones periféricas\n"
"- PET-CT para estadificación completa"
),
},
{
"header": "Estratificación de Riesgo",
"content": (
"La estratificación del riesgo de malignidad en nódulos pulmonares "
"solitarios se basa en los criterios de Fleischner Society:\n\n"
"Nódulos Sólidos:\n"
"- <6 mm: No requiere seguimiento (bajo riesgo)\n"
"- 6-8 mm: TC de seguimiento a los 6-12 meses\n"
"- >8 mm: Considerar PET-CT, biopsia o seguimiento a los 3 meses\n\n"
"Factores que aumentan la probabilidad de malignidad:\n"
"- Bordes espiculados (VPP >90%)\n"
"- Localización en lóbulo superior\n"
"- Crecimiento documentado\n"
"- Antecedente de tabaquismo (>30 paquetes-año)\n"
"- Edad >60 años"
),
},
{
"header": "Tratamiento - Estadio I-II (Enfermedad Temprana)",
"content": (
"Recomendación: Resección quirúrgica es el tratamiento de elección.\n\n"
"Opciones quirúrgicas:\n"
"- Lobectomía (estándar de cuidado)\n"
"- Segmentectomía (para tumores ≤2 cm periféricos)\n"
"- Neumonectomía (si es necesaria para márgenes negativos)\n\n"
"Quimioterapia adyuvante:\n"
"- Estadio IA: No recomendada\n"
"- Estadio IB (tumores >4 cm): Considerar cisplatino-vinorelbina\n"
"- Estadio II: Cisplatino-vinorelbina × 4 ciclos (Evidencia Nivel 1A)\n\n"
"Radioterapia:\n"
"- SBRT para pacientes inoperables con Estadio I"
),
},
{
"header": "Tratamiento - Estadio III (Enfermedad Localmente Avanzada)",
"content": (
"Recomendación: Quimioradioterapia concurrente seguida de immunoterapia.\n\n"
"Protocolo estándar:\n"
"1. Quimioradioterapia concurrente con cisplatino-etopósido\n"
"2. Consolidación con durvalumab × 12 meses (Ensayo PACIFIC)\n\n"
"Estadio IIIA resecable:\n"
"- Considerar quimioterapia neoadyuvante + cirugía\n"
"- Nivolumab neoadyuvante + quimioterapia (CheckMate 816)\n\n"
"Evidencia: Durvalumab post-QRT mejora la supervivencia global "
"a 5 años del 33.4% al 42.9% (HR 0.72, IC 95% 0.59-0.89)"
),
},
{
"header": "Tratamiento - Estadio IV (Enfermedad Metastásica)",
"content": (
"Recomendación: Terapia sistémica basada en biomarcadores.\n\n"
"Testing molecular obligatorio:\n"
"- EGFR, ALK, ROS1, BRAF V600E, KRAS G12C, MET, RET, NTRK\n"
"- PD-L1 (TPS) mediante inmunohistoquímica\n\n"
"Primera línea según biomarcador:\n"
"- EGFR mutado: Osimertinib (Evidencia 1A)\n"
"- ALK fusión: Alectinib (Evidencia 1A)\n"
"- PD-L1 ≥50%: Pembrolizumab monoterapia\n"
"- PD-L1 <50%, sin drivers: Pembrolizumab + pemetrexed + platino\n"
"- KRAS G12C: Sotorasib o adagrasib"
),
},
{
"header": "Evidencia - Ensayos Clínicos de Referencia",
"content": (
"1. KEYNOTE-024: Pembrolizumab vs quimioterapia en PD-L1 ≥50%. "
"SLP: 10.3 vs 6.0 meses (HR 0.50). SG: 30.0 vs 14.2 meses.\n"
"2. FLAURA: Osimertinib vs gefitinib/erlotinib en EGFR+. "
"SLP: 18.9 vs 10.2 meses (HR 0.46).\n"
"3. ALEX: Alectinib vs crizotinib en ALK+. "
"SLP: 34.8 vs 10.9 meses (HR 0.43).\n"
"4. CheckMate 816: Nivolumab neoadyuvante + QT. "
"pCR: 24.0% vs 2.2% (OR 13.94).\n"
"5. PACIFIC: Durvalumab post-QRT en Estadio III. "
"SG a 5 años: 42.9% vs 33.4% (HR 0.72)."
),
},
],
"breast_cancer": [
{
"header": "Diagnóstico",
"content": (
"Evaluación inicial ante sospecha de cáncer de mama:\n"
"- Mamografía bilateral diagnóstica\n"
"- Ecografía mamaria complementaria\n"
"- RM mamaria en alto riesgo (BRCA1/2, historia familiar)\n"
"- Biopsia con aguja gruesa (core) guiada por imagen\n"
"- Panel inmunohistoquímico: ER, PR, HER2, Ki-67\n"
"- Testing genómico: Oncotype DX, MammaPrint (Estadio I-II, HR+)"
),
},
{
"header": "Estratificación por Subtipo Molecular",
"content": (
"Clasificación molecular y pronóstico:\n\n"
"Luminal A (HR+/HER2-, Ki67 bajo):\n"
"- Mejor pronóstico, responde a hormonoterapia\n"
"- Tratamiento: Tamoxifeno o inhibidores de aromatasa\n\n"
"Luminal B (HR+/HER2-, Ki67 alto):\n"
"- Requiere quimioterapia adyuvante + hormonoterapia\n\n"
"HER2-positivo:\n"
"- Trastuzumab + pertuzumab + quimioterapia\n"
"- T-DM1 si enfermedad residual post-neoadyuvancia\n\n"
"Triple Negativo (TNBC):\n"
"- Quimioterapia con antraciclina + taxano\n"
"- Pembrolizumab neoadyuvante (KEYNOTE-522)"
),
},
{
"header": "Tratamiento - Enfermedad Temprana",
"content": (
"Recomendación: Cirugía + terapia adyuvante según subtipo.\n\n"
"Cirugía conservadora + radioterapia es equivalente a mastectomía "
"en supervivencia global (Nivel de Evidencia 1A).\n\n"
"Biopsia de ganglio centinela para axila clínicamente negativa.\n"
"Disección axilar solo si ≥3 ganglios positivos.\n\n"
"Hormonoterapia adyuvante (HR+):\n"
"- Premenopausia: Tamoxifeno × 5-10 años\n"
"- Postmenopausia: Letrozol/Anastrozol × 5 años\n"
"- CDK4/6 inhibidor (abemaciclib) si alto riesgo"
),
},
{
"header": "Evidencia - Ensayos Clínicos de Referencia",
"content": (
"1. KEYNOTE-522: Pembrolizumab neoadyuvante en TNBC. "
"pCR: 64.8% vs 51.2% (delta 13.6%). SLE a 3 años mejorada.\n"
"2. monarchE: Abemaciclib adyuvante en HR+/HER2- alto riesgo. "
"iDFS a 4 años: 85.8% vs 79.4% (HR 0.664).\n"
"3. CLEOPATRA: Pertuzumab + trastuzumab en HER2+ metastásico. "
"SG: 56.5 vs 40.8 meses (HR 0.68).\n"
"4. DESTINY-Breast04: T-DXd en HER2-low. "
"SLP: 10.1 vs 5.4 meses (HR 0.51)."
),
},
],
"colorectal_cancer": [
{
"header": "Diagnóstico",
"content": (
"Evaluación diagnóstica del cáncer colorrectal:\n"
"- Colonoscopia completa con biopsia de la lesión\n"
"- TC de tórax/abdomen/pelvis con contraste para estadificación\n"
"- CEA sérico basal\n"
"- RM pélvica para cáncer de recto\n"
"- Testing molecular: MSI/MMR, KRAS, NRAS, BRAF, HER2"
),
},
{
"header": "Estratificación de Riesgo",
"content": (
"Factores pronósticos en cáncer colorrectal:\n\n"
"Alto riesgo de recurrencia:\n"
"- T4 (invasión de serosa o órganos adyacentes)\n"
"- Invasión linfovascular o perineural\n"
"- Histología pobremente diferenciada\n"
"- <12 ganglios linfáticos examinados\n"
"- Márgenes positivos o cercanos (<1 mm)\n"
"- Obstrucción o perforación intestinal al diagnóstico\n\n"
"MSI-High (dMMR):\n"
"- Mejor pronóstico en Estadio II\n"
"- No beneficio de 5-FU en monoterapia\n"
"- Excelente respuesta a inmunoterapia en enfermedad avanzada"
),
},
{
"header": "Tratamiento - Enfermedad Localizada",
"content": (
"Recomendación: Resección quirúrgica con márgenes adecuados.\n\n"
"Colon:\n"
"- Colectomía con linfadenectomía (mínimo 12 ganglios)\n"
"- Estadio II alto riesgo: Considerar FOLFOX × 3-6 meses\n"
"- Estadio III: FOLFOX o CAPOX × 3-6 meses (Evidencia 1A)\n\n"
"Recto:\n"
"- cT3/T4 o N+: Quimioradioterapia neoadyuvante (TNT preferida)\n"
"- Total Neoadjuvant Therapy (TNT): FOLFOX × 4 → QRT → cirugía\n"
"- Respuesta clínica completa: Considerar Watch-and-Wait"
),
},
{
"header": "Tratamiento - Enfermedad Metastásica",
"content": (
"Recomendación: Terapia sistémica guiada por biomarcadores.\n\n"
"MSI-High/dMMR:\n"
"- Primera línea: Pembrolizumab monoterapia (KEYNOTE-177)\n"
"- SLP: 16.5 vs 8.2 meses (HR 0.60)\n\n"
"MSS (microsatélite estable):\n"
"- RAS wild-type, lado izquierdo: FOLFOX/FOLFIRI + cetuximab\n"
"- RAS wild-type, lado derecho: FOLFOX/FOLFIRI + bevacizumab\n"
"- RAS mutado: FOLFOX/FOLFIRI + bevacizumab\n"
"- BRAF V600E: Encorafenib + cetuximab (BEACON CRC)\n\n"
"Metástasis hepáticas resecables:\n"
"- Quimioterapia perioperatoria + resección hepática\n"
"- Supervivencia a 5 años: 30-50% post-resección"
),
},
{
"header": "Evidencia - Ensayos Clínicos de Referencia",
"content": (
"1. KEYNOTE-177: Pembrolizumab en CCR MSI-H primera línea. "
"SLP: 16.5 vs 8.2 meses (HR 0.60). ORR: 43.8% vs 33.1%.\n"
"2. BEACON CRC: Encorafenib + cetuximab en BRAF V600E. "
"SG: 9.3 vs 5.9 meses (HR 0.61).\n"
"3. IDEA: CAPOX × 3 meses vs 6 meses en Estadio III. "
"No-inferioridad demostrada para bajo riesgo.\n"
"4. RAPIDO: TNT en cáncer de recto localmente avanzado. "
"Fallo del tratamiento: 23.7% vs 30.4% (HR 0.75)."
),
},
],
}
def generate_guideline_pdf(
cancer_type: str,
output_dir: str = "data/clinical_guides",
) -> str:
"""
Generates a synthetic clinical guideline PDF for testing the RAG pipeline.
Uses reportlab if available, falls back to PyMuPDF (fitz) plain text PDF.
Args:
cancer_type: Key from SYNTHETIC_GUIDELINES dict.
output_dir: Directory to save the generated PDF.
Returns:
Absolute path to the generated PDF file.
"""
if cancer_type not in SYNTHETIC_GUIDELINES:
raise ValueError(
f"Unknown cancer type '{cancer_type}'. "
f"Available: {list(SYNTHETIC_GUIDELINES.keys())}"
)
sections = SYNTHETIC_GUIDELINES[cancer_type]
os.makedirs(output_dir, exist_ok=True)
output_path = os.path.join(output_dir, f"synthetic_guideline_{cancer_type}.pdf")
try:
_generate_with_reportlab(cancer_type, sections, output_path)
except ImportError:
_generate_with_fitz(cancer_type, sections, output_path)
print(f"✅ Generated synthetic guideline PDF → {output_path}")
return os.path.abspath(output_path)
def _generate_with_reportlab(
cancer_type: str,
sections: List[Dict[str, str]],
output_path: str,
) -> None:
"""Generate PDF using reportlab for richer formatting."""
from reportlab.lib.pagesizes import letter
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import inch
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer
doc = SimpleDocTemplate(output_path, pagesize=letter)
styles = getSampleStyleSheet()
title_style = ParagraphStyle(
"GuidelineTitle",
parent=styles["Title"],
fontSize=18,
spaceAfter=20,
)
header_style = ParagraphStyle(
"SectionHeader",
parent=styles["Heading2"],
fontSize=14,
spaceBefore=16,
spaceAfter=8,
)
body_style = ParagraphStyle(
"BodyText",
parent=styles["BodyText"],
fontSize=10,
leading=14,
)
elements: list = []
title = cancer_type.replace("_", " ").title()
elements.append(Paragraph(f"Guía Clínica Sintética: {title}", title_style))
elements.append(
Paragraph(
"DOCUMENTO SINTÉTICO - Solo para validación del pipeline OncoAgent",
styles["Italic"],
)
)
elements.append(Spacer(1, 0.3 * inch))
for section in sections:
elements.append(Paragraph(section["header"], header_style))
# Convert newlines to
for reportlab
content_html = section["content"].replace("\n", "
")
elements.append(Paragraph(content_html, body_style))
elements.append(Spacer(1, 0.2 * inch))
doc.build(elements)
def _generate_with_fitz(
cancer_type: str,
sections: List[Dict[str, str]],
output_path: str,
) -> None:
"""Fallback: Generate plain-text PDF using PyMuPDF (fitz)."""
import fitz # PyMuPDF
doc = fitz.open()
title = cancer_type.replace("_", " ").title()
for section in sections:
page = doc.new_page()
text = f"{section['header']}\n\n{section['content']}"
text_rect = fitz.Rect(50, 50, 550, 750)
page.insert_textbox(text_rect, text, fontsize=11, fontname="helv")
# Add a title page at the beginning
title_page = doc.new_page(pno=0)
title_rect = fitz.Rect(50, 200, 550, 400)
title_page.insert_textbox(
title_rect,
f"Guía Clínica Sintética: {title}\n\n"
"DOCUMENTO SINTÉTICO\nSolo para validación del pipeline OncoAgent",
fontsize=16,
fontname="helv",
align=1, # Center
)
doc.save(output_path)
doc.close()
def generate_all_guidelines(output_dir: str = "data/clinical_guides") -> List[str]:
"""
Generates synthetic guideline PDFs for all cancer types.
Returns:
List of absolute paths to generated PDF files.
"""
paths: List[str] = []
for cancer_type in SYNTHETIC_GUIDELINES:
path = generate_guideline_pdf(cancer_type, output_dir)
paths.append(path)
return paths
if __name__ == "__main__":
generated = generate_all_guidelines()
print(f"\n🚀 Generated {len(generated)} synthetic guideline PDFs:")
for p in generated:
print(f" → {p}")