gauravmeena0708/epfo-circulars
0
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 