Cognitive-rag / 01_data_ingestion / ingest_mtsamples.py
ingest_mtsamples.py
Raw
#!/usr/bin/env python
"""Ingest and clean the MTSamples CSV into structured JSONL."""

from __future__ import annotations

import argparse
import csv
import json
import os
import re
from collections import Counter
from datetime import datetime, timezone
from typing import Dict, Iterable, List, Tuple

SECTION_RE = re.compile(r"([A-Z][A-Z0-9 /&\-]{1,}):")


def normalize_whitespace(text: str) -> str:
    text = text.replace("\u0000", "").replace("\ufffd", "")
    text = text.replace("\r\n", "\n").replace("\r", "\n")
    text = re.sub(r"\s+", " ", text)
    return text.strip()


def clean_section_content(text: str) -> str:
    text = normalize_whitespace(text)
    text = re.sub(r"([.!?])\s*,", r"\1", text)
    text = re.sub(r"[ ,;:]+$", "", text)
    return text


def split_sections(text: str) -> List[Tuple[str, str]]:
    matches = list(SECTION_RE.finditer(text))
    if not matches:
        cleaned = clean_section_content(text)
        return [("FULL_TEXT", cleaned)] if cleaned else []

    sections: List[Tuple[str, str]] = []
    for idx, match in enumerate(matches):
        start = match.end()
        end = matches[idx + 1].start() if idx + 1 < len(matches) else len(text)
        heading = match.group(1).strip()
        content = text[start:end]
        content = content.lstrip(" ,;:-\n\t")
        content = clean_section_content(content)
        if content:
            sections.append((heading, content))
    return sections


def build_doc_id(row: Dict[str, str], fallback_index: int) -> str:
    raw_id = (row.get("") or "").strip()
    if not raw_id:
        raw_id = str(fallback_index)
    return f"mtsamples_{raw_id}"


def iter_mtsamples(path: str) -> Iterable[Dict[str, str]]:
    with open(path, newline="", encoding="utf-8") as handle:
        reader = csv.DictReader(handle)
        for row in reader:
            yield row


def ingest(input_path: str, output_dir: str) -> Dict[str, object]:
    os.makedirs(output_dir, exist_ok=True)
    output_path = os.path.join(output_dir, "cleaned_mtsamples.jsonl")

    section_counts = Counter()
    doc_count = 0
    section_count = 0
    empty_section_count = 0

    with open(output_path, "w", encoding="utf-8") as out_handle:
        for idx, row in enumerate(iter_mtsamples(input_path)):
            doc_id = build_doc_id(row, idx)
            transcription = (row.get("transcription") or "").strip()
            sections = split_sections(transcription)

            if not sections and transcription:
                empty_section_count += 1

            for section_idx, (section, content) in enumerate(sections):
                record = {
                    "doc_id": doc_id,
                    "page": None,
                    "section": section,
                    "section_index": section_idx,
                    "content": content,
                    "source": {
                        "filename": os.path.basename(input_path),
                        "row_id": row.get("") or str(idx),
                        "sample_name": (row.get("sample_name") or "").strip(),
                        "medical_specialty": (row.get("medical_specialty") or "").strip(),
                        "description": (row.get("description") or "").strip(),
                        "keywords": (row.get("keywords") or "").strip(),
                    },
                }
                out_handle.write(json.dumps(record, ensure_ascii=True) + "\n")
                section_counts[section] += 1
                section_count += 1

            doc_count += 1

    stats = {
        "input_file": os.path.abspath(input_path),
        "output_file": os.path.abspath(output_path),
        "generated_at": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
        "document_count": doc_count,
        "section_count": section_count,
        "empty_section_count": empty_section_count,
        "top_sections": section_counts.most_common(10),
        "notes": [
            "Page numbers are not present in the MTSamples CSV; page is emitted as null.",
        ],
    }

    stats_path = os.path.join(output_dir, "ingestion_stats.json")
    with open(stats_path, "w", encoding="utf-8") as stats_handle:
        json.dump(stats, stats_handle, indent=2, ensure_ascii=True)

    return stats


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="Ingest and clean MTSamples CSV.")
    parser.add_argument(
        "--input",
        default=os.path.join("00_Data", "mtsamples.csv"),
        help="Path to mtsamples.csv",
    )
    parser.add_argument(
        "--output-dir",
        default=os.path.join("01_data_ingestion", "output"),
        help="Output directory for JSONL and stats.",
    )
    return parser.parse_args()


def main() -> None:
    args = parse_args()
    ingest(args.input, args.output_dir)


if __name__ == "__main__":
    main()