CoolFace
Apppublic

gauravmeena0708/epfo-circulars

sourceHugging Faceupdated 26d agoView on Hugging Face
0likes
docx_export.py382 linesDownload Raw Back to root
1"""Generates formatted Microsoft Word (.docx) documents for research reports,2interactive Q&A transcripts, and extracted OCR text.3"""4 5from __future__ import annotations6 7from datetime import datetime8import io9import re10from typing import Any, Mapping, Sequence11 12import docx13from docx.enum.table import WD_ALIGN_VERTICAL, WD_TABLE_ALIGNMENT14from docx.enum.text import WD_ALIGN_PARAGRAPH15from docx.oxml import OxmlElement, parse_xml16from docx.oxml.ns import nsdecls, qn17from docx.shared import Inches, Pt, RGBColor18 19 20NAVY_COLOR = RGBColor(26, 54, 93)  # #1A365D21GRAY_COLOR = RGBColor(100, 116, 139)  # #64748B22DARK_TEXT_COLOR = RGBColor(30, 41, 59)  # #1E293B23 24 25def _set_cell_background(cell, fill_hex: str):26    """Sets the background color of a table cell."""27    tc_pr = cell._tc.get_or_add_tcPr()28    shd = parse_xml(f'<w:shd {nsdecls("w")} w:fill="{fill_hex}"/>')29    tc_pr.append(shd)30 31 32def _set_cell_margins(cell, top=100, bottom=100, left=150, right=150):33    """Sets inner padding/margins for a table cell in dxa (1/20 pt)."""34    tc_pr = cell._tc.get_or_add_tcPr()35    tc_mar = parse_xml(36        f'<w:tcMar {nsdecls("w")}>'37        f'<w:top w:w="{top}" w:type="dxa"/>'38        f'<w:bottom w:w="{bottom}" w:type="dxa"/>'39        f'<w:left w:w="{left}" w:type="dxa"/>'40        f'<w:right w:w="{right}" w:type="dxa"/>'41        f'</w:tcMar>'42    )43    tc_pr.append(tc_mar)44 45 46def _add_formatted_text(paragraph, text: str):47    """Parses simple inline markdown (bold **text**, italics *text*) into Word runs."""48    # Split by bold markers **...**49    tokens = re.split(r"(\*\*.*?\*\*)", text)50    for token in tokens:51        if token.startswith("**") and token.endswith("**") and len(token) >= 4:52            run = paragraph.add_run(token[2:-2])53            run.bold = True54        else:55            # Check for *italic*56            sub_tokens = re.split(r"(\*.*?\*)", token)57            for st in sub_tokens:58                if st.startswith("*") and st.endswith("*") and len(st) >= 2:59                    run = paragraph.add_run(st[1:-1])60                    run.italic = True61                else:62                    paragraph.add_run(st)63 64 65def _render_markdown_blocks(doc: docx.Document, content: str):66    """Appends paragraphs, bullet points, and headers from a markdown string."""67    lines = content.split("\n")68    in_code_block = False69 70    for line in lines:71        stripped = line.strip()72        if stripped.startswith("```"):73            in_code_block = not in_code_block74            continue75 76        if not stripped:77            continue78 79        if in_code_block:80            p = doc.add_paragraph()81            p.paragraph_format.left_indent = Inches(0.3)82            p.paragraph_format.space_after = Pt(2)83            run = p.add_run(line)84            run.font.name = "Consolas"85            run.font.size = Pt(9.5)86            run.font.color.rgb = GRAY_COLOR87            continue88 89        if stripped.startswith("### "):90            p = doc.add_heading(level=3)91            p.paragraph_format.space_before = Pt(8)92            p.paragraph_format.space_after = Pt(3)93            _add_formatted_text(p, stripped[4:])94        elif stripped.startswith("## "):95            p = doc.add_heading(level=2)96            p.paragraph_format.space_before = Pt(12)97            p.paragraph_format.space_after = Pt(4)98            _add_formatted_text(p, stripped[3:])99        elif stripped.startswith("# "):100            p = doc.add_heading(level=1)101            p.paragraph_format.space_before = Pt(16)102            p.paragraph_format.space_after = Pt(6)103            _add_formatted_text(p, stripped[2:])104        elif stripped.startswith(("- ", "* ", "• ")):105            p = doc.add_paragraph(style="List Bullet")106            p.paragraph_format.space_after = Pt(3)107            _add_formatted_text(p, stripped[2:])108        elif re.match(r"^\d+\.\s", stripped):109            match = re.match(r"^\d+\.\s", stripped)110            p = doc.add_paragraph(style="List Number")111            p.paragraph_format.space_after = Pt(3)112            _add_formatted_text(p, stripped[match.end():])113        else:114            p = doc.add_paragraph()115            p.paragraph_format.space_after = Pt(6)116            p.paragraph_format.line_spacing = 1.15117            _add_formatted_text(p, stripped)118 119 120def create_research_report_docx(121    query: str,122    answer_text: str,123    source_references: Sequence[Mapping[str, Any]],124    generated_at: str | None = None,125) -> bytes:126    """127    Creates a styled Word (.docx) document for an EPFO research & citation report.128    """129    doc = docx.Document()130 131    # Configure Margins132    for section in doc.sections:133        section.top_margin = Inches(1.0)134        section.bottom_margin = Inches(1.0)135        section.left_margin = Inches(1.0)136        section.right_margin = Inches(1.0)137 138    # Document Header Title139    title_p = doc.add_paragraph()140    title_p.paragraph_format.space_before = Pt(0)141    title_p.paragraph_format.space_after = Pt(4)142    run_title = title_p.add_run("EPFO Knowledge Assistant — Research & Citation Report")143    run_title.font.name = "Calibri"144    run_title.font.size = Pt(20)145    run_title.bold = True146    run_title.font.color.rgb = NAVY_COLOR147 148    # Timestamp & Metadata Subtitle149    date_str = generated_at or datetime.now().strftime("%Y-%m-%d %H:%M:%S")150    sub_p = doc.add_paragraph()151    sub_p.paragraph_format.space_after = Pt(12)152    sub_run = sub_p.add_run(f"Generated on {date_str}")153    sub_run.font.size = Pt(9.5)154    sub_run.font.color.rgb = GRAY_COLOR155 156    # Query Card / Box157    q_table = doc.add_table(rows=1, cols=1)158    q_table.alignment = WD_TABLE_ALIGNMENT.CENTER159    q_table.autofit = False160    cell = q_table.rows[0].cells[0]161    cell.width = Inches(6.5)162    _set_cell_background(cell, "F1F5F9")  # Slate-100163    _set_cell_margins(cell, top=140, bottom=140, left=180, right=180)164 165    qp = cell.paragraphs[0]166    qp.paragraph_format.space_after = Pt(0)167    q_label = qp.add_run("Research Query: ")168    q_label.bold = True169    q_label.font.color.rgb = NAVY_COLOR170    qp.add_run(query)171 172    doc.add_paragraph().paragraph_format.space_after = Pt(8)173 174    # Section 1: Synthesized Answer175    h1 = doc.add_heading(level=1)176    h1_run = h1.add_run("💡 Synthesized Answer & Analysis")177    h1_run.font.color.rgb = NAVY_COLOR178    h1.paragraph_format.space_before = Pt(12)179    h1.paragraph_format.space_after = Pt(6)180 181    if answer_text and answer_text.strip():182        _render_markdown_blocks(doc, answer_text.strip())183    else:184        empty_p = doc.add_paragraph()185        empty_run = empty_p.add_run("No AI synthesized answer generated (Search & Citations mode).")186        empty_run.italic = True187        empty_run.font.color.rgb = GRAY_COLOR188 189    doc.add_paragraph().paragraph_format.space_after = Pt(8)190 191    # Section 2: Source References & Citations192    if source_references:193        h2 = doc.add_heading(level=1)194        h2_run = h2.add_run(f"📚 Source References ({len(source_references)})")195        h2_run.font.color.rgb = NAVY_COLOR196        h2.paragraph_format.space_before = Pt(14)197        h2.paragraph_format.space_after = Pt(8)198 199        for i, ref in enumerate(source_references, start=1):200            ref_box = doc.add_table(rows=1, cols=1)201            ref_box.alignment = WD_TABLE_ALIGNMENT.CENTER202            ref_cell = ref_box.rows[0].cells[0]203            ref_cell.width = Inches(6.5)204            _set_cell_background(ref_cell, "F8FAFC")205            _set_cell_margins(ref_cell, top=120, bottom=120, left=160, right=160)206 207            rp = ref_cell.paragraphs[0]208            rp.paragraph_format.space_after = Pt(3)209 210            title = ref.get("title") or ref.get("subject") or f"Reference #{i}"211            source = ref.get("source") or ref.get("circular_no") or "Official EPFO Document"212            date = ref.get("date") or ref.get("circular_date") or "N/A"213            score = ref.get("score")214 215            title_run = rp.add_run(f"#{i} {title}\n")216            title_run.bold = True217            title_run.font.size = Pt(11)218            title_run.font.color.rgb = NAVY_COLOR219 220            meta_line = f"Source: {source} | Date: {date}"221            if score is not None:222                try:223                    meta_line += f" | Relevance: {float(score):.2f}"224                except (ValueError, TypeError):225                    pass226 227            meta_p = ref_cell.add_paragraph()228            meta_p.paragraph_format.space_after = Pt(4)229            m_run = meta_p.add_run(meta_line)230            m_run.font.size = Pt(9)231            m_run.font.color.rgb = GRAY_COLOR232 233            snippet = ref.get("text") or ref.get("snippet") or ""234            if snippet:235                snip_p = ref_cell.add_paragraph()236                snip_p.paragraph_format.space_after = Pt(2)237                s_run = snip_p.add_run(f'"{snippet.strip()}"')238                s_run.italic = True239                s_run.font.size = Pt(9.5)240 241            url = ref.get("url") or ref.get("link")242            if url:243                url_p = ref_cell.add_paragraph()244                url_p.paragraph_format.space_after = Pt(0)245                u_run = url_p.add_run(f"Official Link: {url}")246                u_run.font.size = Pt(8.5)247                u_run.font.color.rgb = RGBColor(37, 99, 235)  # Blue248 249            doc.add_paragraph().paragraph_format.space_after = Pt(4)250 251    buf = io.BytesIO()252    doc.save(buf)253    return buf.getvalue()254 255 256def create_chat_transcript_docx(257    title: str,258    chat_history: Sequence[Mapping[str, str]],259    metadata: Mapping[str, Any] | None = None,260) -> bytes:261    """262    Creates a styled Word (.docx) document for conversational Q&A or data analysis sessions.263    """264    doc = docx.Document()265 266    for section in doc.sections:267        section.top_margin = Inches(1.0)268        section.bottom_margin = Inches(1.0)269        section.left_margin = Inches(1.0)270        section.right_margin = Inches(1.0)271 272    # Document Header Title273    title_p = doc.add_paragraph()274    title_p.paragraph_format.space_after = Pt(4)275    run_title = title_p.add_run(title)276    run_title.font.name = "Calibri"277    run_title.font.size = Pt(18)278    run_title.bold = True279    run_title.font.color.rgb = NAVY_COLOR280 281    date_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")282    sub_p = doc.add_paragraph()283    sub_p.paragraph_format.space_after = Pt(10)284    sub_run = sub_p.add_run(f"Generated on {date_str}")285    sub_run.font.size = Pt(9.5)286    sub_run.font.color.rgb = GRAY_COLOR287 288    # Optional Metadata Table289    if metadata:290        meta_table = doc.add_table(rows=len(metadata), cols=2)291        meta_table.alignment = WD_TABLE_ALIGNMENT.CENTER292        for row_idx, (k, v) in enumerate(metadata.items()):293            row = meta_table.rows[row_idx]294            k_cell, v_cell = row.cells[0], row.cells[1]295            k_cell.width = Inches(2.0)296            v_cell.width = Inches(4.5)297            _set_cell_background(k_cell, "F1F5F9")298            _set_cell_background(v_cell, "F8FAFC")299            _set_cell_margins(k_cell, 80, 80, 100, 100)300            _set_cell_margins(v_cell, 80, 80, 100, 100)301 302            kp = k_cell.paragraphs[0]303            k_run = kp.add_run(str(k))304            k_run.bold = True305            k_run.font.size = Pt(9.5)306 307            vp = v_cell.paragraphs[0]308            v_run = vp.add_run(str(v))309            v_run.font.size = Pt(9.5)310 311        doc.add_paragraph().paragraph_format.space_after = Pt(8)312 313    doc.add_paragraph().paragraph_format.space_after = Pt(4)314 315    # Render Chat Conversation316    for i, msg in enumerate(chat_history):317        role = msg.get("role", "user").lower()318        content = msg.get("content", "")319 320        is_user = role == "user"321        role_label = "👤 User Query" if is_user else "🤖 Analysis & Response"322 323        head = doc.add_heading(level=2)324        head.paragraph_format.space_before = Pt(12)325        head.paragraph_format.space_after = Pt(4)326        h_run = head.add_run(role_label)327        h_run.font.color.rgb = RGBColor(30, 64, 175) if is_user else NAVY_COLOR328 329        _render_markdown_blocks(doc, content)330        doc.add_paragraph().paragraph_format.space_after = Pt(4)331 332    buf = io.BytesIO()333    doc.save(buf)334    return buf.getvalue()335 336 337def create_text_document_docx(338    title: str,339    body_text: str,340    metadata: Mapping[str, Any] | None = None,341) -> bytes:342    """343    Creates a styled Word (.docx) document from raw extracted text.344    """345    doc = docx.Document()346 347    for section in doc.sections:348        section.top_margin = Inches(1.0)349        section.bottom_margin = Inches(1.0)350        section.left_margin = Inches(1.0)351        section.right_margin = Inches(1.0)352 353    title_p = doc.add_paragraph()354    title_p.paragraph_format.space_after = Pt(4)355    run_title = title_p.add_run(title)356    run_title.font.name = "Calibri"357    run_title.font.size = Pt(18)358    run_title.bold = True359    run_title.font.color.rgb = NAVY_COLOR360 361    date_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")362    sub_p = doc.add_paragraph()363    sub_p.paragraph_format.space_after = Pt(12)364    sub_run = sub_p.add_run(f"Exported on {date_str}")365    sub_run.font.size = Pt(9.5)366    sub_run.font.color.rgb = GRAY_COLOR367 368    if metadata:369        for k, v in metadata.items():370            mp = doc.add_paragraph()371            mp.paragraph_format.space_after = Pt(2)372            k_run = mp.add_run(f"{k}: ")373            k_run.bold = True374            mp.add_run(str(v))375        doc.add_paragraph().paragraph_format.space_after = Pt(6)376 377    _render_markdown_blocks(doc, body_text)378 379    buf = io.BytesIO()380    doc.save(buf)381    return buf.getvalue()382