From 28daff58be0e0c862174165d5580f78f0247a08d Mon Sep 17 00:00:00 2001 From: Chaim Date: Thu, 16 Apr 2026 18:49:10 +0000 Subject: [PATCH] Pre-existing agent updates + analysis DOCX export MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updates accumulated from prior sessions: - HEARTBEAT: company-based filtering (CMP/CMPA) rules - legal-qa, legal-researcher: routine updates - analysis_docx_exporter: new service for analysis DOCX export - compose page: "הורד כ-DOCX" button for analysis - decision_template.docx: template for exporter Co-Authored-By: Claude Opus 4.7 (1M context) --- .claude/agents/HEARTBEAT.md | 35 +- .claude/agents/legal-qa.md | 8 + .claude/agents/legal-researcher.md | 8 + .../services/analysis_docx_exporter.py | 503 ++++++++++++++++++ scripts/convert_decision_template.py | 102 ++++ skills/docx/decision_template.docx | Bin 0 -> 29951 bytes .../app/cases/[caseNumber]/compose/page.tsx | 12 + 7 files changed, 665 insertions(+), 3 deletions(-) create mode 100644 mcp-server/src/legal_mcp/services/analysis_docx_exporter.py create mode 100644 scripts/convert_decision_template.py create mode 100644 skills/docx/decision_template.docx diff --git a/.claude/agents/HEARTBEAT.md b/.claude/agents/HEARTBEAT.md index 3137ef4..a45ff42 100644 --- a/.claude/agents/HEARTBEAT.md +++ b/.claude/agents/HEARTBEAT.md @@ -15,10 +15,25 @@ הרץ את הרשימה הזו בכל heartbeat. -## 1. זיהוי +## 1. זיהוי וסינון חברה - וודא שאתה יודע מי אתה: `$PAPERCLIP_AGENT_ID` - בדוק הקשר: `$PAPERCLIP_TASK_ID`, `$PAPERCLIP_WAKE_REASON` +- **זהה את החברה שלך**: `$PAPERCLIP_COMPANY_ID` + +### ⚠️ סינון תיקים לפי חברה — כלל ברזל + +**אתה אחראי רק על תיקים ששייכים לחברה שלך.** הספרה הראשונה של מספר התיק קובעת: + +| חברה | COMPANY_ID | סוגי תיקים | טווח מספרים | +|------|------------|-------------|-------------| +| ועדת ערר רישוי ובניה | `42a7acd0-30c5-4cbd-ac97-7424f65df294` | רישוי ובניה | **1xxx** | +| ועדת ערר היטלי השבחה | `8639e837-4c9d-47fa-a76b-95788d651896` | היטל השבחה + פיצויים ס' 197 | **8xxx, 9xxx** | + +- אם `$PAPERCLIP_COMPANY_ID` = `42a7acd0...` → עבוד רק על תיקים שמתחילים ב-**1** +- אם `$PAPERCLIP_COMPANY_ID` = `8639e837...` → עבוד רק על תיקים שמתחילים ב-**8** או **9** +- **לעולם אל תיצור פרויקט, issue, או תוכן לתיק שלא בטווח שלך** +- אם issue שהוקצה לך מכוון לתיק שלא בטווח שלך — סרב בנימוס ודווח ב-comment ## 2. בדוק תיבת דואר @@ -102,11 +117,25 @@ curl -s -X PATCH -H "Authorization: Bearer $PAPERCLIP_API_KEY" \ **אסור** לסיים issue כ-"done" אם יש כשל שלא טופל. "done" = הכל הושלם בהצלחה. אם משהו נכשל — "blocked". ### 4ג. העֵר את העוזר המשפטי (CEO) — חובה! -אחרי כל סיום משימה (done או blocked), **העֵר את העוזר המשפטי** כדי שיבדוק תוצאות ויחליט על הצעד הבא: +אחרי כל סיום משימה (done או blocked), **העֵר את העוזר המשפטי של החברה שלך** כדי שיבדוק תוצאות ויחליט על הצעד הבא: + +**⚠️ בחר CEO לפי חברה:** +| חברה | COMPANY_ID | CEO Agent ID | +|------|------------|-------------| +| רישוי ובניה (CMP) | `42a7acd0-...` | `752cebdd-6748-4a04-aacd-c7ab0294ef33` | +| היטלי השבחה (CMPA) | `8639e837-...` | `cdbfa8bc-3d61-41a4-a2e7-677ec7d34562` | + ```bash +# קבע CEO_ID לפי חברה: +if [ "$PAPERCLIP_COMPANY_ID" = "8639e837-4c9d-47fa-a76b-95788d651896" ]; then + CEO_ID="cdbfa8bc-3d61-41a4-a2e7-677ec7d34562" +else + CEO_ID="752cebdd-6748-4a04-aacd-c7ab0294ef33" +fi + curl -s -X POST -H "Authorization: Bearer $PAPERCLIP_API_KEY" \ -H "Content-Type: application/json" \ - "$PAPERCLIP_API_URL/api/agents/752cebdd-6748-4a04-aacd-c7ab0294ef33/wakeup" \ + "$PAPERCLIP_API_URL/api/agents/$CEO_ID/wakeup" \ -d '{"source":"automation","triggerDetail":"system","reason":"סוכן [שמך] סיים משימה [issue-id] בסטטוס [done/blocked]","payload":{"issueId":"[issue-id]","mutation":"agent_completion"}}' ``` diff --git a/.claude/agents/legal-qa.md b/.claude/agents/legal-qa.md index 5f63596..a66179b 100644 --- a/.claude/agents/legal-qa.md +++ b/.claude/agents/legal-qa.md @@ -24,6 +24,14 @@ tools: עבוד תמיד בעברית. +## סינון תיקים לפי חברה + +⚠️ **אתה אחראי רק על תיקים ששייכים לחברה שלך** (`$PAPERCLIP_COMPANY_ID`): +- CMP (`42a7acd0-...`) → רק תיקים **1xxx** (רישוי ובניה) +- CMPA (`8639e837-...`) → רק תיקים **8xxx, 9xxx** (היטל השבחה / פיצויים) + +אם issue מכוון לתיק שלא בטווח שלך — סרב ודווח ב-comment. + ## 6 בדיקות ### 1. שלמות מבנית (structural_integrity) diff --git a/.claude/agents/legal-researcher.md b/.claude/agents/legal-researcher.md index a417beb..d1e70dc 100644 --- a/.claude/agents/legal-researcher.md +++ b/.claude/agents/legal-researcher.md @@ -27,6 +27,14 @@ tools: עבוד תמיד בעברית. +## סינון תיקים לפי חברה + +⚠️ **אתה אחראי רק על תיקים ששייכים לחברה שלך** (`$PAPERCLIP_COMPANY_ID`): +- CMP (`42a7acd0-...`) → רק תיקים **1xxx** (רישוי ובניה) +- CMPA (`8639e837-...`) → רק תיקים **8xxx, 9xxx** (היטל השבחה / פיצויים) + +אם issue מכוון לתיק שלא בטווח שלך — סרב ודווח ב-comment. + ## לפני שאתה מתחיל — קרא! 1. **מתודולוגיה אנליטית**: `docs/decision-methodology.md` — במיוחד סעיפים ד.2 (התחל מלשון הטקסט), ד.3 (שלושה מקורות להנחה עליונה), ז (ציטוטים ואזכורי פסיקה) diff --git a/mcp-server/src/legal_mcp/services/analysis_docx_exporter.py b/mcp-server/src/legal_mcp/services/analysis_docx_exporter.py new file mode 100644 index 0000000..86bebf1 --- /dev/null +++ b/mcp-server/src/legal_mcp/services/analysis_docx_exporter.py @@ -0,0 +1,503 @@ +"""Export the legal analysis (analysis-and-research.md + precedents) to a +DOCX file that uses דפנה's decision template styles. + +The template lives at `skills/docx/decision_template.docx` (converted once +from `טיוטת החלטה.dotx` via `scripts/convert_decision_template.py`). +We open it, wipe the sample body paragraphs, and write new content by +applying style names only — never by hand-setting font/size/RTL/margins, +because the template's styles.xml already carries those. + +Style mapping: + "Title" → the document title (case number, date) + "Heading 2" → top-level section headers + (טענות סף / סוגיות להכרעה / מסקנות) + "Normal" + bold → subsection headers (individual claim/issue) + "Normal" → field label (bold run) + value + "Quote" → precedent quote text + "Normal" (italic) → precedent citation + +Output: data/cases/{case_number}/exports/ניתוח-משפטי-v{N}.docx +""" + +from __future__ import annotations + +import re +from pathlib import Path +from typing import Any +from uuid import UUID + +from docx import Document +from docx.document import Document as DocumentT +from docx.oxml.ns import qn +from docx.oxml import OxmlElement +from docx.text.paragraph import Paragraph +from docx.text.run import Run + +from legal_mcp import config +from legal_mcp.services import db, research_md + + +def _mark_run_rtl(run: Run) -> None: + """Mark a run as complex-script (Hebrew/Arabic) so Word uses the `cs` + font slot from the style (David) rather than `ascii` (Times New Roman). + + Without this, runs we add programmatically render Hebrew in the ascii + font — even though the paragraph style has ``. + """ + rPr = run._r.get_or_add_rPr() + if rPr.find(qn("w:rtl")) is None: + rPr.append(OxmlElement("w:rtl")) + + +def _mark_paragraph_rtl(paragraph: Paragraph) -> None: + """Add `` inside the paragraph's rPr so the paragraph mark + itself is treated as RTL. The paragraph style already sets bidi + direction, but empty paragraphs and trailing marks need this flag. + """ + pPr = paragraph._p.get_or_add_pPr() + rPr = pPr.find(qn("w:rPr")) + if rPr is None: + rPr = OxmlElement("w:rPr") + pPr.append(rPr) + if rPr.find(qn("w:rtl")) is None: + rPr.append(OxmlElement("w:rtl")) + +# Path to the converted template. Static — populated by +# scripts/convert_decision_template.py. +TEMPLATE_PATH = ( + Path(__file__).resolve().parents[4] + / "skills" + / "docx" + / "decision_template.docx" +) + +CHAIR_POSITION_LABEL = "עמדת ועדת הערר" +CHAIR_POSITION_PLACEHOLDER = "[טרם מולאה עמדת ועדת הערר]" + +NUMBERED_LINE_RE = re.compile(r"^\s*(\d+)[.)]\s+(.+)$") +BULLET_LINE_RE = re.compile(r"^\s*[\-\u2022\*\u25CF\u25E6]\s+(.+)$") +# (א) (ב) (ג) ... — Hebrew-letter enumeration used by the authors. +# We keep the marker inside the text (the author wrote it), but render the +# paragraph as "List Paragraph" without the numPr so the visual indentation +# matches the template's list style without adding a double "1." prefix. +HEB_LETTER_LINE_RE = re.compile(r"^\s*\([א-ת]\)\s+") + +# A standalone **LABEL:** line (the whole trimmed line is wrapped in ** **) +STANDALONE_LABEL_RE = re.compile(r"^\s*\*\*([^\n*]+?):\*\*\s*$") +# A short standalone "XYZ:" line (no ** **) — acts as a sub-heading for the +# paragraphs that follow. Limit to short phrases to avoid eating real +# sentences that happen to end with a colon. +PLAIN_LABEL_RE = re.compile(r"^\s*([^\n:]{2,40}):\s*$") +# "**LABEL:** value" inline — bold label followed by prose on the same line. +INLINE_LABEL_RE = re.compile(r"^\s*\*\*([^\n*]+?):\*\*\s+(.+)$") + + +def _classify_line(line: str) -> tuple[str, str]: + """Return (kind, clean_text) where kind ∈ {numbered, bullet, heb_letter, + label_heading, inline_label, plain}. + + clean_text conventions: + - numbered/bullet — marker stripped + - heb_letter — marker kept (author supplied it) + - label_heading — surrounding ** and trailing : stripped + - inline_label — "LABEL\x00VALUE" (NUL-separated; _emit splits it) + """ + m = STANDALONE_LABEL_RE.match(line) + if m: + return "label_heading", m.group(1).strip() + m = INLINE_LABEL_RE.match(line) + if m: + return "inline_label", f"{m.group(1).strip()}\x00{m.group(2).strip()}" + m = NUMBERED_LINE_RE.match(line) + if m: + return "numbered", m.group(2).strip() + m = BULLET_LINE_RE.match(line) + if m: + inner = m.group(1).strip() + # A bullet whose only content is **LABEL:** is a heading, not a list item. + # E.g. "- **נקודות פתוחות:**" + m2 = STANDALONE_LABEL_RE.match(inner) + if m2: + return "label_heading", m2.group(1).strip() + # A bullet of the form "- **LABEL:** value" → inline label. + m3 = INLINE_LABEL_RE.match(inner) + if m3: + return "inline_label", f"{m3.group(1).strip()}\x00{m3.group(2).strip()}" + return "bullet", inner + if HEB_LETTER_LINE_RE.match(line): + return "heb_letter", line.strip() + m = PLAIN_LABEL_RE.match(line) + if m: + return "label_heading", m.group(1).strip() + return "plain", line.strip() + + +def _strip_numpr(paragraph: Paragraph) -> None: + """Remove any from the paragraph's pPr. + + Used when we want the visual styling of `List Paragraph` (indent, + font) without Word's auto-decimal "1." prefix — e.g. for Hebrew- + letter enumeration where the author wrote (א) (ב) (ג) manually. + """ + pPr = paragraph._p.get_or_add_pPr() + for numPr in pPr.findall(qn("w:numPr")): + pPr.remove(numPr) + + +# Characters that the code should never emit (user instruction: "no dashes"). +# Applied only to code-generated text, not to user content from the md file. +_CODE_DASH_RE = re.compile(r"[\u2013\u2014]") + +# Markdown inline bold — `**...**` +_INLINE_BOLD_RE = re.compile(r"\*\*([^\n*]+?)\*\*") + + +def _no_dash(text: str) -> str: + """Strip em/en dashes from text the code emits (not from source content).""" + return _CODE_DASH_RE.sub("", text) + + +def _add_runs_with_inline_bold(paragraph: Paragraph, text: str) -> None: + """Split `text` on `**...**` markers, adding alternating plain and bold + runs to `paragraph`. All runs are marked RTL and passed through + `_no_dash`. + + This keeps `**טענה חשובה**` rendering as bold (as the author intended) + instead of leaving the literal asterisks in the output. + """ + text = _no_dash(text) + pos = 0 + for m in _INLINE_BOLD_RE.finditer(text): + if m.start() > pos: + plain = paragraph.add_run(text[pos : m.start()]) + _mark_run_rtl(plain) + bold = paragraph.add_run(m.group(1)) + bold.bold = True + _mark_run_rtl(bold) + pos = m.end() + if pos < len(text): + tail = paragraph.add_run(text[pos:]) + _mark_run_rtl(tail) + + +def _clear_body(doc: DocumentT) -> None: + """Remove every paragraph currently in the document body. + + The template ships with example paragraphs ("רקע", "דיון והכרעה"…) + that we don't want in the output. Section properties (sectPr) are + kept so page size / margins / RTL / footer remain intact. + """ + body = doc.element.body + for p in list(body.findall(qn("w:p"))): + body.remove(p) + # Leave sectPr alone — it carries page setup including bidi. + + +def _add_paragraph(doc: DocumentT, text: str, style: str) -> Paragraph: + p = doc.add_paragraph(style=style) + _mark_paragraph_rtl(p) + if text: + _add_runs_with_inline_bold(p, text) + return p + + +def _add_label_value( + doc: DocumentT, label: str, value: str, *, value_italic: bool = False +) -> Paragraph: + """Add a paragraph with a bold label and an inline value. + + Example rendering: **עמדת המבקשת:** The party argues that… + """ + p = doc.add_paragraph(style="Normal") + _mark_paragraph_rtl(p) + run_label = p.add_run(f"{_no_dash(label)}: ") + run_label.bold = True + _mark_run_rtl(run_label) + if value: + if value_italic: + # Placeholder text — italic, no inline-bold handling. + run_value = p.add_run(_no_dash(value)) + run_value.italic = True + _mark_run_rtl(run_value) + else: + _add_runs_with_inline_bold(p, value) + return p + + +def _add_multiline_value( + doc: DocumentT, label: str, value: str +) -> None: + """Render a field (label + value). + + Multi-line values get the label as its own Heading 2 paragraph (so the + structure visually breaks between fields), then each body line as its + own paragraph routed through `_emit_content_line`. + + Single-line values stay inline (bold label + text) — a Heading 2 for + a one-liner would look inflated. + """ + lines = [ln for ln in value.splitlines() if ln.strip()] + if not lines: + _add_label_value(doc, label, "") + return + if len(lines) == 1: + kind, text = _classify_line(lines[0]) + # Single-line — inline with label regardless of kind + _add_label_value(doc, label, text) + return + # Multi-line: label as Heading 2, then each line via _emit_content_line + _add_paragraph(doc, label, "Heading 2") + for line in lines: + _emit_content_line(doc, line) + + +def _emit_content_line(doc: DocumentT, line: str) -> None: + """Render a single line of content using the right template style. + + - `label_heading` (e.g. "**נקודות פתוחות:**" alone) → Heading 2 + - `numbered` ("1. ...") → List Paragraph + (auto-decimal) + - `heb_letter` ("(א) ...") → List Paragraph + with numPr stripped + (author supplied + the marker) + - `bullet` ("- ...") → Normal (marker + stripped) + - `plain` → Normal + """ + kind, text = _classify_line(line) + + if kind == "label_heading": + _add_paragraph(doc, text, "Heading 2") + return + + if kind == "inline_label": + label, value = text.split("\x00", 1) + _add_label_value(doc, label, value) + return + + if kind == "numbered": + para = doc.add_paragraph(style="List Paragraph") + elif kind == "heb_letter": + para = doc.add_paragraph(style="List Paragraph") + _strip_numpr(para) + else: + para = doc.add_paragraph(style="Normal") + _mark_paragraph_rtl(para) + _add_runs_with_inline_bold(para, text) + + +def _format_subsection_title(item: dict[str, Any], kind_label: str) -> str: + """Return '{kind_label} {number}: {title}' e.g. 'טענת סף 1: חוסר סמכות'.""" + number = item.get("number") or "" + title = item.get("title", "").strip() + if number and title: + return f"{kind_label} {number}: {title}" + if title: + return title + return f"{kind_label} {number}".strip() + + +def _write_subsection( + doc: DocumentT, + item: dict[str, Any], + precedents_for_item: list[dict[str, Any]], + kind_label: str, +) -> None: + # Subsection header — bolded Normal paragraph, not a Heading, + # so it visually sits under the section's Heading 2. + header_text = _format_subsection_title(item, kind_label) + p = doc.add_paragraph(style="Normal") + _mark_paragraph_rtl(p) + run = p.add_run(_no_dash(header_text)) + run.bold = True + _mark_run_rtl(run) + + # Regular fields (party positions, legal questions, etc.) + for field in item.get("fields", []): + label = field.get("label", "").strip() + content = field.get("content", "").strip() + if not label: + continue + _add_multiline_value(doc, label, content) + + # Chair position — special handling: always render, use placeholder if empty. + chair_position = (item.get("chair_position") or "").strip() + if chair_position: + _add_multiline_value(doc, CHAIR_POSITION_LABEL, chair_position) + else: + _add_label_value( + doc, CHAIR_POSITION_LABEL, CHAIR_POSITION_PLACEHOLDER, + value_italic=True, + ) + + # Precedents attached to this subsection + if precedents_for_item: + p = doc.add_paragraph(style="Normal") + _mark_paragraph_rtl(p) + run = p.add_run("פסיקה רלוונטית:") + run.bold = True + _mark_run_rtl(run) + for prec in precedents_for_item: + quote = (prec.get("quote") or "").strip() + citation = (prec.get("citation") or "").strip() + if quote: + _add_paragraph(doc, quote, "Quote") + if citation: + cite_p = doc.add_paragraph(style="Normal") + _mark_paragraph_rtl(cite_p) + cite_run = cite_p.add_run(_no_dash(citation)) + cite_run.italic = True + _mark_run_rtl(cite_run) + + +def _add_background_section( + doc: DocumentT, title: str, body: str | None +) -> None: + """Render a background H2 section (e.g. "רקע דיוני") from a prose + body. Lines are routed through `_emit_content_line` so bullets, + `**labels:**`, and (א) enumerations all get the template styles. + """ + if not body or not body.strip(): + return + _add_paragraph(doc, title, "Heading 2") + for raw in body.splitlines(): + if not raw.strip(): + continue + _emit_content_line(doc, raw) + + +def _group_precedents( + precedents: list[dict[str, Any]], +) -> tuple[list[dict], dict[str, list[dict]]]: + """Split the flat precedent list into case-level and per-section maps. + + Returns (case_level_precedents, {section_id: [precedents]}). + """ + case_level: list[dict] = [] + by_section: dict[str, list[dict]] = {} + for p in precedents: + sid = p.get("section_id") + if sid is None: + case_level.append(p) + else: + by_section.setdefault(sid, []).append(p) + return case_level, by_section + + +def _next_version(export_dir: Path) -> int: + """Return the next version number for ניתוח-משפטי-v{N}.docx.""" + existing = sorted(export_dir.glob("ניתוח-משפטי-v*.docx")) + next_ver = 1 + for p in existing: + try: + ver = int(p.stem.split("-v")[1]) + except (IndexError, ValueError): + continue + next_ver = max(next_ver, ver + 1) + return next_ver + + +async def build_analysis_docx(case_number: str) -> Path: + """Build a DOCX of the legal analysis for a case using the template + styles, and save a versioned copy under the case's exports folder. + + Raises FileNotFoundError if no analysis file or template exists. + """ + if not TEMPLATE_PATH.exists(): + raise FileNotFoundError( + f"Template not found at {TEMPLATE_PATH}. " + "Run: python scripts/convert_decision_template.py" + ) + + case_dir = config.find_case_dir(case_number) + analysis_path = case_dir / "documents" / "research" / "analysis-and-research.md" + if not analysis_path.exists(): + raise FileNotFoundError( + f"Analysis file not found for case {case_number}" + ) + + parsed = research_md.parse(analysis_path) + + # Resolve case_id so we can fetch precedents. Missing case → proceed + # without precedents rather than failing the export. + case_level_precedents: list[dict] = [] + precedents_by_section: dict[str, list[dict]] = {} + case = await db.get_case_by_number(case_number) + if case: + precedents = await db.list_case_precedents(UUID(case["id"])) + case_level_precedents, precedents_by_section = _group_precedents(precedents) + + doc = Document(str(TEMPLATE_PATH)) + _clear_body(doc) + + # Document title + header = parsed.get("header", {}) + date = header.get("date", "").strip() + title_text = f"ניתוח משפטי וכתיבת עמדה בערר {case_number}" + _add_paragraph(doc, title_text, "Heading 1") + if date: + p_date = doc.add_paragraph(style="Normal") + _mark_paragraph_rtl(p_date) + run_date = p_date.add_run(f"תאריך: {date}") + _mark_run_rtl(run_date) + + # Background sections — printed first so the reader gets context + # before any claims/precedents. These come only in the exported DOCX, + # not in the web UI (the UI renders them elsewhere). + _add_background_section(doc, "רקע לניתוח", parsed.get("represented_party")) + _add_background_section(doc, "רקע דיוני", parsed.get("procedural_background")) + _add_background_section(doc, "עובדות מוסכמות", parsed.get("agreed_facts")) + _add_background_section( + doc, "עובדות שנויות במחלוקת", parsed.get("disputed_facts") + ) + + # Case-level precedents appear at the top (they cut across claims/issues) + if case_level_precedents: + _add_paragraph(doc, "פסיקה כללית", "Heading 2") + for prec in case_level_precedents: + quote = (prec.get("quote") or "").strip() + citation = (prec.get("citation") or "").strip() + if quote: + _add_paragraph(doc, quote, "Quote") + if citation: + cp = doc.add_paragraph(style="Normal") + _mark_paragraph_rtl(cp) + cr = cp.add_run(_no_dash(citation)) + cr.italic = True + _mark_run_rtl(cr) + + # Threshold claims + threshold_claims = parsed.get("threshold_claims", []) + if threshold_claims: + _add_paragraph(doc, "טענות סף", "Heading 2") + for tc in threshold_claims: + _write_subsection( + doc, tc, precedents_by_section.get(tc["id"], []), "טענת סף" + ) + + # Issues + issues = parsed.get("issues", []) + if issues: + _add_paragraph(doc, "סוגיות להכרעה", "Heading 2") + for iss in issues: + _write_subsection( + doc, iss, precedents_by_section.get(iss["id"], []), "סוגיה" + ) + + # Conclusions + conclusions = (parsed.get("conclusions") or "").strip() + if conclusions: + _add_paragraph(doc, "מסקנות", "Heading 2") + for raw in conclusions.splitlines(): + if not raw.strip(): + continue + _emit_content_line(doc, raw) + + # Save versioned + export_dir = case_dir / "exports" + export_dir.mkdir(parents=True, exist_ok=True) + version = _next_version(export_dir) + out_path = export_dir / f"ניתוח-משפטי-v{version}.docx" + doc.save(str(out_path)) + return out_path diff --git a/scripts/convert_decision_template.py b/scripts/convert_decision_template.py new file mode 100644 index 0000000..676e688 --- /dev/null +++ b/scripts/convert_decision_template.py @@ -0,0 +1,102 @@ +"""Convert דפנה's decision .dotx template to a loadable .docx file. + +python-docx cannot open .dotx files directly (content type is +`...template.main+xml` rather than `...document.main+xml`). This script +produces a sibling .docx by rewriting [Content_Types].xml and dropping +the `word/glossary/` part (which is template-specific and can interfere +with plain Document() loading). + +The output preserves every style definition, numbering, fonts, and +section properties — the only things we want from the template. + +Run once (or whenever the source .dotx changes): + + python scripts/convert_decision_template.py + +Input: data/training/טיוטת החלטה.dotx +Output: skills/docx/decision_template.docx +""" + +from __future__ import annotations + +import re +import sys +import zipfile +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent +SRC = REPO_ROOT / "data" / "training" / "טיוטת החלטה.dotx" +DST = REPO_ROOT / "skills" / "docx" / "decision_template.docx" + +TEMPLATE_CONTENT_TYPE = ( + "application/vnd.openxmlformats-officedocument." + "wordprocessingml.template.main+xml" +) +DOCUMENT_CONTENT_TYPE = ( + "application/vnd.openxmlformats-officedocument." + "wordprocessingml.document.main+xml" +) + + +def convert(src: Path, dst: Path) -> None: + if not src.exists(): + raise FileNotFoundError(f"Template not found: {src}") + dst.parent.mkdir(parents=True, exist_ok=True) + + with zipfile.ZipFile(src, "r") as zin: + names = zin.namelist() + with zipfile.ZipFile(dst, "w", zipfile.ZIP_DEFLATED) as zout: + for name in names: + # Drop glossary part — template-only, confuses Document() + if name.startswith("word/glossary/"): + continue + data = zin.read(name) + if name == "[Content_Types].xml": + text = data.decode("utf-8") + text = text.replace( + TEMPLATE_CONTENT_TYPE, DOCUMENT_CONTENT_TYPE + ) + # Drop every that points at /word/glossary/... + text = re.sub( + r']*?/>', + "", + text, + ) + data = text.encode("utf-8") + elif name == "word/_rels/document.xml.rels": + # Strip the glossaryDocument relationship — the target + # part is being removed, so the ref would dangle. + text = data.decode("utf-8") + text = re.sub( + r']*?glossaryDocument[^>]*?/>', + "", + text, + ) + data = text.encode("utf-8") + zout.writestr(name, data) + + +def verify(dst: Path) -> None: + """Load with python-docx and print a few style names to confirm it works.""" + from docx import Document + + doc = Document(str(dst)) + key_styles = {"Normal", "Heading 2", "Quote", "List Paragraph", "Title"} + found = {s.name for s in doc.styles if s.name in key_styles} + missing = key_styles - found + if missing: + print(f"WARN: missing styles: {missing}", file=sys.stderr) + else: + print(f"OK — all key styles present: {sorted(found)}") + + +def main() -> None: + print(f"Source: {SRC}") + print(f"Dest: {DST}") + convert(SRC, DST) + print(f"Wrote {DST.stat().st_size:,} bytes") + verify(DST) + + +if __name__ == "__main__": + main() diff --git a/skills/docx/decision_template.docx b/skills/docx/decision_template.docx new file mode 100644 index 0000000000000000000000000000000000000000..1d5e6ccb557ec1f1aa2f78337c44990a54d46c14 GIT binary patch literal 29951 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 literal 0 HcmV?d00001 diff --git a/web-ui/src/app/cases/[caseNumber]/compose/page.tsx b/web-ui/src/app/cases/[caseNumber]/compose/page.tsx index 6d7dbbd..acc2d0d 100644 --- a/web-ui/src/app/cases/[caseNumber]/compose/page.tsx +++ b/web-ui/src/app/cases/[caseNumber]/compose/page.tsx @@ -103,6 +103,18 @@ function AnalysisActions({ הורד ניתוח )} + {hasAnalysis && ( + + )}