From 1da1585ca8cbd568b8a2015ddf2c40cfc7bcf2e9 Mon Sep 17 00:00:00 2001 From: Chaim Date: Mon, 6 Jul 2026 12:46:32 +0000 Subject: [PATCH] =?UTF-8?q?fix(block-zayin):=20=D7=94=D7=A4=D7=A8=D7=93?= =?UTF-8?q?=D7=AA-=D7=9E=D7=A9=D7=99=D7=91=D7=99=D7=9D=20=D7=91=D7=91?= =?UTF-8?q?=D7=9C=D7=95=D7=A7=20=D7=96'=20(=D7=98=D7=A2=D7=A0=D7=95=D7=AA?= =?UTF-8?q?=20=D7=94=D7=A6=D7=93=D7=93=D7=99=D7=9D)=20=E2=80=94=20=D7=94?= =?UTF-8?q?=D7=A9=D7=9C=D7=9E=D7=AA=20#224=20=D7=9C=D7=A9=D7=9B=D7=91?= =?UTF-8?q?=D7=AA-=D7=94=D7=9B=D7=AA=D7=99=D7=91=D7=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit בלוק ז' של טיוטת-הביניים מיזג את כל המשיבים לסעיף אחד גנרי (או השמיטם) — כי (א) _build_claims_context קיבץ רק לפי party_role, ו-(ב) ה-prompt נעל "מבנה קבוע 3 חלקים: עוררים / ועדה מקומית / מבקשי-היתר" — בלי "משיבים" בכלל. לכן בתיק 1043-02-26 המשיבים 2-3 ו-4-6 לא הופיעו כצדדים מובחנים, אף שהנתונים (129 טענות-משיבים מפוצלות לפי כתב-תשובה) תקינים ב-claims. הפרדת-המשיבים של #224 יושמה בצבירה+פאנל בלבד (PR#392) — לא בכותב-הבלוקים. - `_build_claims_context` מקבץ לפי (party_role, brief) כאשר brief=source_document ל-SPLIT_PARTIES (respondent/permit_applicant) — **אותו כלל בדיוק כמו הצבירה** (import SPLIT_PARTIES מ-argument_aggregator, G2). התוצאה: סעיף נפרד לכל כתב-תשובה ("טענות המשיבים — כתב תשובה משיבות 2 3", "— משיבים 4 6"...). - prompt בלוק ז': מבנה-צדדים דינמי לפי הכותרות בקונטקסט; הוראה מפורשת לא למזג משיבים שונים ולשקף עמדות-מנוגדות; יעד-אורך גמיש לכמה כתבי-תשובה. משפיע גם על בלוק י' (אותו _build_claims_context) — הקשר מופרד. py_compile ירוק. עבודה על MCP → דורש host reload + הרצה-מחדש כדי לראות בפועל. Invariants: G2 (נגזרת-הפרדה יחידה משותפת עם הצבירה), G1. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/legal_mcp/services/block_writer.py | 54 ++++++++++++++----- 1 file changed, 40 insertions(+), 14 deletions(-) diff --git a/mcp-server/src/legal_mcp/services/block_writer.py b/mcp-server/src/legal_mcp/services/block_writer.py index 028a097..4de5853 100644 --- a/mcp-server/src/legal_mcp/services/block_writer.py +++ b/mcp-server/src/legal_mcp/services/block_writer.py @@ -192,16 +192,16 @@ BLOCK_PROMPTS = { ## כללים קריטיים: - **סנתז טענות דומות** — אל תרשום כל טענה בנפרד. קבץ טענות דומות לנושא אחד. למשל: כל הטענות על הודעות → סעיף אחד, כל הטענות על רכוש משותף → סעיף אחד. - גוף שלישי: "העוררים טוענים כי...", "הוועדה המקומית ציינה כי..." -- **מבנה קבוע עם 3 חלקים:** - 1. "טענות העוררים" — 8-12 סעיפים מקובצים לפי נושא - 2. "עמדת הוועדה המקומית" — 5-8 סעיפים - 3. "עמדת מבקשי ההיתר" (אם יש) — 5-10 סעיפים +- **מבנה: חלק (סעיף עם כותרת-משנה) לכל צד שמופיע בטענות שחולצו למטה** — לפי הכותרות שם (### ...). אל תמציא צדדים שאינם מופיעים ואל תשמיט צד שמופיע. + - "טענות העוררים" — 8-12 סעיפים מקובצים לפי נושא. + - **צד-משיב/מתנגד: כתוב חלק נפרד לכל כתב-תשובה** שמופיע (למשל "עמדת משיבות 2-3" ו-"עמדת משיבים 4-6" בנפרד) — 5-10 סעיפים לכל אחד. **אל תמזג משיבים שונים לסעיף אחד**, ואם עמדותיהם מנוגדות — שקף זאת. + - "עמדת הוועדה המקומית" / "עמדת מבקשי ההיתר" (אם מופיעים) — 5-8 סעיפים. - כותרת: "תמצית טענות הצדדים" - נאמנות למקור — לא להמציא טענות, אבל כן לאחד ולסכם טענות חוזרות - אין ניתוח, אין מסקנות, אין הערכה ("טענה חלשה/חזקה") - רק מכתבי טענות מקוריים (לא השלמות טיעון) - מספור רציף -- **יעד אורך: 800-1500 מילים** +- **יעד אורך: 800-1500 מילים** (יותר כשיש כמה כתבי-תשובה נפרדים — כל צד מקבל את מלוא ההתייחסות) ## טענות שחולצו (קבץ טענות דומות לנושאים): {claims_context} @@ -629,18 +629,44 @@ async def _build_claims_context(case_id: UUID) -> str: # Fallback to all non-block-zayin claims if no source claims exist. source_claims = [c for c in claims if c.get("source_document", "") != "block-zayin"] or claims - lines = [] - current_role = "" + # Group by (party_role, brief) so a multi-litigant side (respondent / + # permit_applicant) whose claims come from distinct pleadings reads as + # SEPARATE positions — e.g. "כתב תשובה משיבות 2-3" vs "משיבים 4-6" — instead + # of one merged "טענות המשיבים" (#224). The brief label is the source + # pleading, and which sides split is SPLIT_PARTIES — the SAME rule the + # argument aggregator uses, so block ז and the arguments panel agree (G2). + from legal_mcp.services.argument_aggregator import SPLIT_PARTIES + role_heb = {"appellant": "טענות העוררים", "respondent": "טענות המשיבים", "committee": "עמדת הוועדה המקומית", "permit_applicant": "עמדת מבקשי ההיתר"} - claim_num = 0 + role_order = ["appellant", "committee", "respondent", "permit_applicant"] + + groups: dict[tuple[str, str], list[dict]] = {} for c in source_claims: - if c["party_role"] != current_role: - current_role = c["party_role"] - lines.append(f"\n### {role_heb.get(current_role, current_role)}") - claim_num += 1 - lines.append(f"טענה #{claim_num}: {c['claim_text'][:400]}") - lines.append(f"\n**סה\"כ {claim_num} טענות. ענה על כל טענה מהותית; טענות [bundle] — אגד; טענות [skip] — ציון קצר בלבד.**") + role = c.get("party_role", "") or "" + brief = (c.get("source_document", "") or "").strip() if role in SPLIT_PARTIES else "" + groups.setdefault((role, brief), []).append(c) + + def _sort_key(k: tuple[str, str]) -> tuple[int, str]: + role, brief = k + idx = role_order.index(role) if role in role_order else len(role_order) + return (idx, brief) + + lines: list[str] = [] + claim_num = 0 + for role, brief in sorted(groups.keys(), key=_sort_key): + header = role_heb.get(role, role or "טענות נוספות") + if brief: + header = f"{header} — {brief}" + lines.append(f"\n### {header}") + for c in groups[(role, brief)]: + claim_num += 1 + lines.append(f"טענה #{claim_num}: {c['claim_text'][:400]}") + lines.append( + f"\n**סה\"כ {claim_num} טענות. שמור על ההפרדה בין הצדדים לעיל — " + f"לכל צד/כתב-תשובה עמדה נפרדת (אל תמזג משיבים שונים). ענה על כל טענה " + f"מהותית; טענות [bundle] — אגד; טענות [skip] — ציון קצר בלבד.**" + ) return "\n".join(lines)