diff --git a/.claude/agents/legal-researcher.md b/.claude/agents/legal-researcher.md index b719248..bcd0b3c 100644 --- a/.claude/agents/legal-researcher.md +++ b/.claude/agents/legal-researcher.md @@ -124,6 +124,7 @@ mcp__legal-ai__internal_decision_upload( decision_date="2020-11-15", practice_area="rishuy_uvniya", # Axis B appeal_subtype="building_permit", + proceeding_type="ערר", # 'ערר' / 'בל"מ' — ראה מטה subject_tags=["שימוש חורג"], is_binding=False, # תמיד False — שכנוע אופקי, לא חוב ) @@ -135,6 +136,13 @@ mcp__legal-ai__internal_decision_upload( - `chair_name` — בלעדיו אי-אפשר לחפש סלקטיבית לפי הרכב - `district` — ערכים תקפים: **ירושלים / מרכז / תל אביב / צפון / דרום / חיפה / ארצי** (גם "תל-אביב" עם מקף נקלט) +**שדה מומלץ — `proceeding_type`:** +- `"ערר"` — הליך ערר עיקרי (כותרת ב-PDF: "ערר (ועדות ערר ...) NNNN/YY") +- `'בל"מ'` — בקשה להארכת מועד להגשת ערר (כותרת: "בל\"מ NNNN/YY" או נושא "בקשה להארכת מועד להגשת ערר") +- שני הסוגים יכולים לחלוק אותו מספר תיק (למשל 8047/23 קיים גם כערר וגם כבל"מ). +- בכותרת הראשית של ה-PDF זה תמיד מפורש — לקרוא משם ולא לנחש. +- אם תשאיר ריק — הכלי גוזר אוטומטית מ-appeal_subtype (`extension_request_*` → 'בל"מ') או מתבנית הטקסט. עדיף מפורש. + **הכלי שומר `source_kind='internal_committee'`.** DB constraint `case_law_internal_district_check` אוכף ש-`district NOT NULL` כשמדובר ב-internal_committee. ### אם chair_name או district חסר ב-PDF diff --git a/mcp-server/src/legal_mcp/services/db.py b/mcp-server/src/legal_mcp/services/db.py index 6d9f9d6..4a52686 100644 --- a/mcp-server/src/legal_mcp/services/db.py +++ b/mcp-server/src/legal_mcp/services/db.py @@ -823,6 +823,58 @@ CREATE TABLE IF NOT EXISTS legal_argument_propositions ( """ +# proceeding_type — מבחין בין הליך ערר עיקרי לבל"מ (בקשה להארכת מועד). +# חל גם על case_law (קורפוס) וגם על cases (תיקים חיים). שני הסוגים +# יכולים לחלוק אותו case_number, ולכן ה-uniqueness עוברת ל-(case_number, +# proceeding_type). בקורפוס: רק internal_committee מקבלים ערך מאוכלס; +# פסיקה חיצונית נשארת עם ''. +SCHEMA_V15_SQL = """ +-- ------- case_law (קורפוס) ------- +ALTER TABLE case_law ADD COLUMN IF NOT EXISTS proceeding_type TEXT NOT NULL DEFAULT ''; + +ALTER TABLE case_law DROP CONSTRAINT IF EXISTS case_law_proceeding_type_check; +ALTER TABLE case_law ADD CONSTRAINT case_law_proceeding_type_check + CHECK (proceeding_type IN ('', 'ערר', 'בל"מ')); + +-- Backfill לפי appeal_subtype הקיים +UPDATE case_law SET proceeding_type = 'בל"מ' + WHERE source_kind = 'internal_committee' AND proceeding_type = '' + AND appeal_subtype LIKE 'extension_request_%'; + +UPDATE case_law SET proceeding_type = 'ערר' + WHERE source_kind = 'internal_committee' AND proceeding_type = ''; + +ALTER TABLE case_law DROP CONSTRAINT IF EXISTS case_law_internal_proceeding_check; +ALTER TABLE case_law ADD CONSTRAINT case_law_internal_proceeding_check + CHECK (source_kind != 'internal_committee' OR proceeding_type IN ('ערר', 'בל"מ')); + +-- החלפת UNIQUE(case_number) ב-partial unique לפי source_kind +ALTER TABLE case_law DROP CONSTRAINT IF EXISTS case_law_case_number_key; +DROP INDEX IF EXISTS case_law_case_number_key; +CREATE UNIQUE INDEX IF NOT EXISTS uq_case_law_internal_number_proc + ON case_law (case_number, proceeding_type) + WHERE source_kind = 'internal_committee'; +CREATE UNIQUE INDEX IF NOT EXISTS uq_case_law_external_number + ON case_law (case_number) + WHERE source_kind <> 'internal_committee'; + +-- ------- cases (תיקים חיים) ------- +ALTER TABLE cases ADD COLUMN IF NOT EXISTS proceeding_type TEXT NOT NULL DEFAULT 'ערר'; + +ALTER TABLE cases DROP CONSTRAINT IF EXISTS cases_proceeding_type_check; +ALTER TABLE cases ADD CONSTRAINT cases_proceeding_type_check + CHECK (proceeding_type IN ('ערר', 'בל"מ')); + +UPDATE cases SET proceeding_type = 'בל"מ' + WHERE proceeding_type = 'ערר' AND appeal_subtype LIKE 'extension_request_%'; + +ALTER TABLE cases DROP CONSTRAINT IF EXISTS cases_case_number_key; +DROP INDEX IF EXISTS cases_case_number_key; +CREATE UNIQUE INDEX IF NOT EXISTS uq_cases_number_proc + ON cases (case_number, proceeding_type); +""" + + async def _run_schema_migrations(pool: asyncpg.Pool) -> None: async with pool.acquire() as conn: await conn.execute(SCHEMA_SQL) @@ -840,7 +892,8 @@ async def _run_schema_migrations(pool: asyncpg.Pool) -> None: await conn.execute(SCHEMA_V12_SQL) await conn.execute(SCHEMA_V13_SQL) await conn.execute(SCHEMA_V14_SQL) - logger.info("Database schema initialized (v1-v14)") + await conn.execute(SCHEMA_V15_SQL) + logger.info("Database schema initialized (v1-v15)") async def init_schema() -> None: @@ -867,6 +920,7 @@ async def create_case( # from the case_number prefix before calling here. practice_area: str = "", appeal_subtype: str = "", + proceeding_type: str = "ערר", ) -> dict: pool = await get_pool() case_id = uuid4() @@ -875,14 +929,14 @@ async def create_case( """INSERT INTO cases (id, case_number, title, appellants, respondents, subject, property_address, permit_number, committee_type, hearing_date, notes, expected_outcome, - practice_area, appeal_subtype) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)""", + practice_area, appeal_subtype, proceeding_type) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)""", case_id, case_number, title, json.dumps(appellants or []), json.dumps(respondents or []), subject, property_address, permit_number, committee_type, hearing_date, notes, expected_outcome, - practice_area, appeal_subtype, + practice_area, appeal_subtype, proceeding_type, ) return await get_case(case_id) @@ -2024,18 +2078,22 @@ async def create_internal_committee_decision( summary: str = "", is_binding: bool = True, document_id: UUID | None = None, + proceeding_type: str = "ערר", ) -> dict: """Upsert an appeals-committee decision as source_kind='internal_committee'. - If a row with this case_number already exists as cited_only, promotes it. - Idempotent: calling again updates metadata in-place. + Idempotency key: (case_number, proceeding_type) — the same number can + exist as both 'ערר' and 'בל"מ' (an extension-of-time request can be + filed against an existing appeal with the same number). """ pool = await get_pool() tags_json = json.dumps(subject_tags or [], ensure_ascii=False) async with pool.acquire() as conn: existing = await conn.fetchrow( - "SELECT id FROM case_law WHERE case_number = $1", - case_number, + "SELECT id FROM case_law " + "WHERE case_number = $1 AND proceeding_type = $2 " + " AND source_kind = 'internal_committee'", + case_number, proceeding_type, ) if existing: row = await conn.fetchrow( @@ -2072,19 +2130,20 @@ async def create_internal_committee_decision( subject_tags, summary, full_text, source_kind, source_type, document_id, extraction_status, halacha_extraction_status, - practice_area, appeal_subtype, is_binding + practice_area, appeal_subtype, is_binding, proceeding_type ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, 'internal_committee', 'appeals_committee', $10, 'processing', 'pending', - $11, $12, $13 + $11, $12, $13, $14 ) RETURNING * """, case_number, case_name, court, decision_date, chair_name, district, tags_json, summary, full_text, document_id, practice_area, appeal_subtype, is_binding, + proceeding_type, ) return _row_to_case_law(row) @@ -2100,6 +2159,7 @@ async def update_case_law(case_law_id: UUID, **fields) -> dict | None: "case_number", "case_name", "court", "date", "practice_area", "appeal_subtype", "subject_tags", "summary", "headnote", "key_quote", "source_url", "source_type", "precedent_level", "is_binding", "district", "chair_name", + "proceeding_type", } updates = {k: v for k, v in fields.items() if k in allowed} if not updates: diff --git a/mcp-server/src/legal_mcp/services/internal_decisions.py b/mcp-server/src/legal_mcp/services/internal_decisions.py index 2f8bf66..f2013bf 100644 --- a/mcp-server/src/legal_mcp/services/internal_decisions.py +++ b/mcp-server/src/legal_mcp/services/internal_decisions.py @@ -24,6 +24,7 @@ from uuid import UUID, uuid4 from legal_mcp import config from legal_mcp.services import chunker, db, embeddings, extractor +from legal_mcp.services.practice_area import derive_proceeding_type logger = logging.getLogger(__name__) @@ -86,11 +87,13 @@ async def ingest_internal_decision( text: str | None = None, document_id: UUID | None = None, queue_halachot: bool = True, + proceeding_type: str = "", ) -> dict: """Ingest an appeals-committee decision into the internal corpus. Either file_path or text must be provided. If district is empty, it is inferred from court. + If proceeding_type is empty, it is derived from appeal_subtype/case_name. Returns: {"status": "completed", "case_law_id": "...", "chunks": N} """ if not file_path and not text: @@ -99,6 +102,9 @@ async def ingest_internal_decision( raise ValueError("case_number is required") resolved_district = district.strip() or _district_from_court(court) + resolved_proc = proceeding_type.strip() or derive_proceeding_type( + appeal_subtype=appeal_subtype, subject=case_name, + ) if file_path: src = Path(file_path) @@ -133,6 +139,7 @@ async def ingest_internal_decision( summary=summary.strip(), is_binding=is_binding, document_id=document_id, + proceeding_type=resolved_proc, ) case_law_id = UUID(str(record["id"])) diff --git a/mcp-server/src/legal_mcp/services/practice_area.py b/mcp-server/src/legal_mcp/services/practice_area.py index 88979dc..40673e1 100644 --- a/mcp-server/src/legal_mcp/services/practice_area.py +++ b/mcp-server/src/legal_mcp/services/practice_area.py @@ -263,6 +263,18 @@ def is_blam_subtype(appeal_subtype: str) -> bool: return appeal_subtype in BLAM_SUBTYPES +def derive_proceeding_type(*, appeal_subtype: str = "", subject: str = "") -> str: + """Return 'בל"מ' / 'ערר' for appeals-committee decisions/cases. + + Priority: explicit subtype prefix → subject regex → default 'ערר'. + """ + if appeal_subtype and appeal_subtype.startswith("extension_request_"): + return 'בל"מ' + if subject and is_blam_subject(subject): + return 'בל"מ' + return "ערר" + + def derive_domain_practice_area(case_number: str) -> str: """Map a case_number prefix to a domain practice_area (axis B). diff --git a/mcp-server/src/legal_mcp/services/precedent_metadata_extractor.py b/mcp-server/src/legal_mcp/services/precedent_metadata_extractor.py index 89757de..6adbc2b 100644 --- a/mcp-server/src/legal_mcp/services/precedent_metadata_extractor.py +++ b/mcp-server/src/legal_mcp/services/precedent_metadata_extractor.py @@ -50,6 +50,7 @@ METADATA_EXTRACTION_PROMPT = """אתה מסייע משפטי בכיר. קרא א "decision_date_iso": "YYYY-MM-DD — תאריך מתן ההחלטה כפי שמופיע בטקסט (בכותרת או בחתימה הסופית). אם לא ניתן לזהות במדויק — מחרוזת ריקה.", "precedent_level": "אחד מ-4: 'עליון' / 'מנהלי' / 'ועדת_ערר_ארצית' / 'ועדת_ערר_מחוזית'. בחר לפי הערכאה שמסומנת בכותרת הפסק. אם לא ברור — מחרוזת ריקה.", "source_type": "אחד מ-2: 'court_ruling' (פסק דין של בית משפט — עליון/מנהלי) / 'appeals_committee' (החלטה של ועדת ערר). אם לא ברור — מחרוזת ריקה.", + "proceeding_type": "אחד מ-2 (רק להחלטות ועדת ערר): 'ערר' (הליך ערר עיקרי על החלטת ועדה מקומית) / 'בל\\\"מ' (בקשה להארכת מועד להגשת ערר). זהה דרך כותרת המסמך: 'ערר (ועדות ערר ...) NNNN/YY' → 'ערר'; 'בל\\\"מ NNNN/YY' או נושא 'בקשה להארכת מועד להגשת ערר' → 'בל\\\"מ'. בפסיקת בית משפט (לא ועדת ערר) — מחרוזת ריקה.", "court": "שם הערכאה כפי שהוא מופיע בכותרת (למשל 'בית המשפט העליון', 'בית המשפט המחוזי בירושלים בשבתו כבית משפט לעניינים מנהליים', 'ועדת הערר לתכנון ובניה פיצויים והיטלי השבחה — מחוז ירושלים'). מחרוזת ריקה אם לא ניתן לזהות.", "case_number_clean": "מספר הערר/תיק כפי שמופיע בכותרת — רק הספרות והאלכסון, למשל '1062/24' או '8031/21'. ללא המילה 'ערר', ללא שם הצדדים, ללא סוגריים. אם יש כמה עררים מאוחדים — הרשום הראשון. מחרוזת ריקה אם לא ניתן לזהות." } @@ -65,6 +66,7 @@ METADATA_EXTRACTION_PROMPT = """אתה מסייע משפטי בכיר. קרא א 8. **precedent_level** — קבע לפי הערכאה: בית המשפט העליון = "עליון"; בית משפט מחוזי בשבתו כבית משפט לעניינים מנהליים = "מנהלי"; ועדת ערר ארצית = "ועדת_ערר_ארצית"; ועדת ערר מחוזית (כמו ועדות תכנון ובניה ירושלים/מחוז המרכז וכד') = "ועדת_ערר_מחוזית". השתמש ב-underscore כפי שמופיע — לא ברווח. 9. **source_type** — שני ערכים בלבד: "court_ruling" כשהמסמך הוא פסק דין/החלטה של בית משפט (עליון/בג"ץ/מנהלי/מחוזי); "appeals_committee" כשהמסמך הוא החלטה של ועדת ערר (ארצית או מחוזית). זה משלים את `precedent_level` — שני השדות צריכים להיות תואמים. 10. **court** — מהכותרת הראשית של הפסק. ניסוח מלא (לא קיצור). מחרוזת ריקה אם לא ניתן לזהות. +11. **proceeding_type** — חובה לזהות עבור החלטות ועדת ערר; ריק עבור פסיקת בית משפט. הסימן הברור: בכותרת הראשונה של המסמך כתוב "ערר (ועדות ערר ...) NNNN/YY" → 'ערר'; "בל\"מ NNNN/YY" או הנושא "בקשה להארכת מועד להגשת ערר" → 'בל\"מ'. שני הסוגים יכולים לחלוק אותו מספר תיק — לכן חשוב להבחין מפורשות. """ @@ -160,6 +162,10 @@ async def extract_metadata(case_law_id: UUID | str) -> dict: st = result["source_type"].strip() if st in {"court_ruling", "appeals_committee"}: out["source_type"] = st + if isinstance(result.get("proceeding_type"), str): + pt = result["proceeding_type"].strip() + if pt in {"ערר", 'בל"מ', ""}: + out["proceeding_type"] = pt if isinstance(result.get("court"), str): out["court"] = result["court"].strip() if isinstance(result.get("case_number_clean"), str): @@ -267,6 +273,13 @@ async def apply_to_record( if c: fields_to_update["court"] = c + # proceeding_type — only fill for internal_committee rows (the field is + # meaningless for court rulings, which we keep as ''). + if not (record.get("proceeding_type") or "").strip(): + pt = (suggested.get("proceeding_type") or "").strip() + if pt and (record.get("source_kind") == "internal_committee"): + fields_to_update["proceeding_type"] = pt + if overwrite_case_number: cn = (suggested.get("case_number_clean") or "").strip() if cn: diff --git a/mcp-server/src/legal_mcp/tools/cases.py b/mcp-server/src/legal_mcp/tools/cases.py index 7b7915a..8e61a19 100644 --- a/mcp-server/src/legal_mcp/tools/cases.py +++ b/mcp-server/src/legal_mcp/tools/cases.py @@ -130,6 +130,7 @@ async def case_create( expected_outcome: str = "", practice_area: str = "", appeal_subtype: str = "", + proceeding_type: str = "", ) -> str: """יצירת תיק ערר חדש. @@ -150,6 +151,7 @@ async def case_create( אוטומטית ממספר התיק (1xxx→רישוי, 8xxx→השבחה, 9xxx→197) appeal_subtype: סוג ערר (building_permit / betterment_levy / compensation_197). ריק = יוסק אוטומטית ממספר התיק + proceeding_type: 'ערר' / 'בל"מ'. ריק = יוסק מ-appeal_subtype/subject. """ from datetime import date as date_type @@ -173,6 +175,11 @@ async def case_create( appeal_subtype = derived_subtype pa.validate(practice_area, appeal_subtype) + # proceeding_type: explicit override > derived from subtype/subject > 'ערר' + resolved_proc = proceeding_type.strip() or pa.derive_proceeding_type( + appeal_subtype=appeal_subtype, subject=subject, + ) + case = await db.create_case( case_number=case_number, title=title, @@ -187,6 +194,7 @@ async def case_create( expected_outcome=expected_outcome, practice_area=practice_area, appeal_subtype=appeal_subtype, + proceeding_type=resolved_proc, ) # If the user overrode the case-number convention (e.g. case 8500 marked @@ -290,6 +298,7 @@ async def case_update( respondents: list[str] | None = None, property_address: str = "", permit_number: str = "", + proceeding_type: str = "", ) -> str: """עדכון פרטי תיק. @@ -307,6 +316,7 @@ async def case_update( respondents: רשימת משיבים חדשה property_address: כתובת נכס חדשה permit_number: מספר תכנית/בקשה חדש + proceeding_type: 'ערר' / 'בל"מ' — ריק = ללא שינוי """ from datetime import date as date_type @@ -359,6 +369,12 @@ async def case_update( fields["property_address"] = property_address if permit_number: fields["permit_number"] = permit_number + if proceeding_type: + if proceeding_type not in {"ערר", 'בל"מ'}: + raise ValueError( + f"proceeding_type לא תקין: {proceeding_type!r}. ערכים תקפים: ערר / בל\"מ" + ) + fields["proceeding_type"] = proceeding_type updated = await db.update_case(UUID(case["id"]), **fields) diff --git a/mcp-server/src/legal_mcp/tools/internal_decisions.py b/mcp-server/src/legal_mcp/tools/internal_decisions.py index 4105444..fb6b5fa 100644 --- a/mcp-server/src/legal_mcp/tools/internal_decisions.py +++ b/mcp-server/src/legal_mcp/tools/internal_decisions.py @@ -21,6 +21,10 @@ from legal_mcp.services import internal_decisions as int_svc # Valid Hebrew district names (matches _COURT_TO_DISTRICT in service) VALID_DISTRICTS = {"ירושלים", "מרכז", "תל אביב", "תל-אביב", "צפון", "דרום", "חיפה", "ארצי"} +# proceeding_type — ערר vs בל"מ. The service can derive it from +# appeal_subtype/subject if left empty, so this stays optional at the API. +VALID_PROCEEDING_TYPES = {"ערר", 'בל"מ'} + def _ok(payload) -> str: return json.dumps(payload, ensure_ascii=False, indent=2, default=str) @@ -43,6 +47,7 @@ async def internal_decision_upload( subject_tags: list[str] | None = None, summary: str = "", is_binding: bool = False, + proceeding_type: str = "", ) -> str: """העלאת החלטה של ועדת ערר (internal_committee) לקורפוס הסמכותי. @@ -62,6 +67,7 @@ async def internal_decision_upload( appeal_subtype: building_permit / וכו'. subject_tags: תגיות נושא. is_binding: בד"כ False (ועדת ערר לא מחייבת ועדה אחרת — שכנוע אופקי). + proceeding_type: 'ערר' או 'בל"מ'. אם ריק — נגזר מ-appeal_subtype/case_name. Returns: JSON עם case_law_id, מספר chunks, halachot_pending. """ @@ -83,6 +89,11 @@ async def internal_decision_upload( f"district לא תקין: {district!r}. ערכים תקפים: " + ", ".join(sorted(VALID_DISTRICTS)) ) + if proceeding_type.strip() and proceeding_type.strip() not in VALID_PROCEEDING_TYPES: + return _err( + f"proceeding_type לא תקין: {proceeding_type!r}. ערכים תקפים: " + + ", ".join(sorted(VALID_PROCEEDING_TYPES)) + ) try: result = await int_svc.ingest_internal_decision( @@ -98,6 +109,7 @@ async def internal_decision_upload( summary=summary, is_binding=is_binding, file_path=file_path, + proceeding_type=proceeding_type, ) except Exception as e: return _err(str(e)) diff --git a/scripts/SCRIPTS.md b/scripts/SCRIPTS.md index c2fca21..ed3a48f 100644 --- a/scripts/SCRIPTS.md +++ b/scripts/SCRIPTS.md @@ -29,6 +29,7 @@ | `multimodal_backfill.py` | python | Backfill voyage-multimodal-3 page embeddings על מסמכי תיקים קיימים. idempotent (skips by default), forces `MULTIMODAL_ENABLED=true` ל-run, רץ מהקונטיינר. שלב C — ראה `docs/voyage-upgrades-plan.md` | ידני per-case (`python multimodal_backfill.py 8174-24 8137-24`) | | `backfill_chunk_pages.py` | python | Backfill `page_number` ב-`document_chunks` קיימים. legacy chunker לא tracked עמודים → `page_number=NULL` חוסם boost של multimodal hybrid (text+image join על אותו עמוד). re-extracts כל PDF (re-OCR אם צריך, ~$0.0015/page), מחשב page_offsets, ומעדכן chunks. idempotent | ידני per-case (`python backfill_chunk_pages.py 8174-24 8137-24`) | | `backfill_legal_arguments.py` | python | Backfill `legal_arguments` לתיקים עם `claims` קיימים (TaskMaster #36). מקבץ פרופוזיציות גולמיות לטיעונים משפטיים מובחנים (~6-12 לכל צד) דרך `argument_aggregator.aggregate_claims_to_arguments` (Claude CLI). תומך `--dry-run`/`--apply`/`--force`/`--case ...`. **חייב לרוץ מהמכונה המקומית** (לא קונטיינר) — `claude_session` דורש Claude CLI | ידני per-case (`python scripts/backfill_legal_arguments.py --apply --case 1017-03-26`) | +| `upload_blam_decisions.py` | python | חד-פעמי (2026-05-26) — העלאת 2 החלטות בל"מ ל-`case_law` (8126/24 סופר נוח, 8047/23 הרנון) דרך `ingest_internal_decision` ישיר, עוקף MCP server שטרם נטען מחדש אחרי הוספת `proceeding_type`. **לא להריץ שוב** | חד-פעמי — להעביר ל-`.archive/` בהזדמנות | ## תיקיית `.archive/` — סקריפטים שהושלמו diff --git a/scripts/upload_blam_decisions.py b/scripts/upload_blam_decisions.py new file mode 100644 index 0000000..d984b2d --- /dev/null +++ b/scripts/upload_blam_decisions.py @@ -0,0 +1,60 @@ +"""One-shot uploader for the 2 new בל"מ decisions Chaim staged in +data/precedents/incoming/. Bypasses MCP because the running MCP server +was started before SCHEMA_V15 + proceeding_type wiring landed. + +Run from /home/chaim/legal-ai with the venv: + POSTGRES_URL=... .venv/bin/python scripts/upload_blam_decisions.py +""" + +import asyncio +import os +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "mcp-server", "src")) + +from legal_mcp.services import internal_decisions as svc + +DECISIONS = [ + { + "file_path": "/home/chaim/legal-ai/data/precedents/incoming/ARAR-24-8126.pdf", + "case_number": "8126/24", + "chair_name": "דפנה תמיר", + "district": "ירושלים", + "case_name": "הוועדה המקומית ירושלים נ' סופר נוח", + "court": "ועדת הערר לתכנון ובנייה — מחוז ירושלים", + "decision_date": "2024-07-07", + "practice_area": "betterment_levy", + "appeal_subtype": "extension_request_betterment_levy", + "proceeding_type": 'בל"מ', + "subject_tags": ["בקשה_להארכת_מועד", "היטל_השבחה"], + "summary": "", + "is_binding": False, + }, + { + "file_path": "/home/chaim/legal-ai/data/precedents/incoming/ARAR-23-8047-3.docx", + "case_number": "8047/23", + "chair_name": "דפנה תמיר", + "district": "ירושלים", + "case_name": 'עזבון אליהו הרנון ז"ל נ\' הוועדה המקומית ירושלים', + "court": "ועדת הערר לתכנון ובנייה — מחוז ירושלים", + "decision_date": "2025-09-29", + "practice_area": "betterment_levy", + "appeal_subtype": "extension_request_betterment_levy", + "proceeding_type": 'בל"מ', + "subject_tags": ["בקשה_להארכת_מועד", "היטל_השבחה"], + "summary": "", + "is_binding": False, + }, +] + + +async def main(): + for d in DECISIONS: + print(f"→ uploading {d['case_number']} ({d['proceeding_type']})") + result = await svc.ingest_internal_decision(**d) + print(f" ✓ case_law_id={result.get('case_law_id')} chunks={result.get('chunks')}") + print("done.") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/web-ui/src/components/cases/case-edit-dialog.tsx b/web-ui/src/components/cases/case-edit-dialog.tsx index 3ecc0c3..c1e0ae4 100644 --- a/web-ui/src/components/cases/case-edit-dialog.tsx +++ b/web-ui/src/components/cases/case-edit-dialog.tsx @@ -17,7 +17,10 @@ import { } from "@/components/ui/select"; import { PartiesField } from "@/components/wizard/parties-field"; import { useUpdateCase } from "@/lib/api/cases"; -import { caseUpdateSchema, expectedOutcomes, type CaseUpdateInput } from "@/lib/schemas/case"; +import { + caseUpdateSchema, expectedOutcomes, proceedingTypes, + type CaseUpdateInput, +} from "@/lib/schemas/case"; import type { CaseDetail } from "@/lib/api/cases"; /* @@ -47,6 +50,7 @@ export function CaseEditDialog({ data }: { data: CaseDetail }) { respondents: data.respondents ?? [], property_address: data.property_address ?? "", permit_number: data.permit_number ?? "", + proceeding_type: data.proceeding_type ?? "ערר", }, }); @@ -63,6 +67,7 @@ export function CaseEditDialog({ data }: { data: CaseDetail }) { respondents: data.respondents ?? [], property_address: data.property_address ?? "", permit_number: data.permit_number ?? "", + proceeding_type: data.proceeding_type ?? "ערר", }); }, [open, data, form]); @@ -104,6 +109,37 @@ export function CaseEditDialog({ data }: { data: CaseDetail }) { +
+ + ( + + )} + /> +

+ ערר = הליך עיקרי; בל"מ = בקשה להארכת מועד להגשת ערר +

+
+
- ערר {data?.case_number ?? "—"} + {data?.proceeding_type ?? "ערר"} {data?.case_number ?? "—"} {data?.status && } {data?.archived_at && ( diff --git a/web-ui/src/components/wizard/case-wizard.tsx b/web-ui/src/components/wizard/case-wizard.tsx index c234a6f..ca5af04 100644 --- a/web-ui/src/components/wizard/case-wizard.tsx +++ b/web-ui/src/components/wizard/case-wizard.tsx @@ -16,7 +16,7 @@ import { import { PartiesField } from "@/components/wizard/parties-field"; import { useCreateCase } from "@/lib/api/cases"; import { - caseCreateSchema, expectedOutcomes, + caseCreateSchema, expectedOutcomes, proceedingTypes, type CaseCreateInput, } from "@/lib/schemas/case"; import { @@ -35,7 +35,7 @@ type StepKey = (typeof STEPS)[number]["key"]; /* Fields validated at each step — lets the user fix just what's on screen * before moving forward, instead of surfacing all errors from page 1. */ const STEP_FIELDS: Record = { - basics: ["case_number", "title", "practice_area", "appeal_subtype"], + basics: ["case_number", "title", "proceeding_type", "practice_area", "appeal_subtype"], parties: ["appellants", "respondents"], details: ["subject", "hearing_date", "expected_outcome", "notes", "property_address", "permit_number"], }; @@ -66,6 +66,7 @@ export function CaseWizard() { expected_outcome: "", practice_area: "appeals_committee", appeal_subtype: "unknown", + proceeding_type: "ערר", }, }); @@ -74,11 +75,17 @@ export function CaseWizard() { * stop the moment they manually pick a value from the dropdown. Mirrors * the wireSubtypeAutofill() behaviour of the vanilla UI * (legal-ai/web/static/index.html around line 2770). + * + * proceeding_type follows the same pattern: if the user hasn't picked + * a value yet, we derive 'בל"מ' whenever the subtype lands on an + * extension_request_* value. */ const userTouchedSubtype = useRef(false); + const userTouchedProceeding = useRef(false); const caseNumber = form.watch("case_number"); const practiceArea = form.watch("practice_area"); const subject = form.watch("subject"); + const appealSubtype = form.watch("appeal_subtype"); useEffect(() => { if (userTouchedSubtype.current) return; /* derive_subtype_with_blam picks extension_request_* when subject @@ -89,6 +96,16 @@ export function CaseWizard() { } }, [caseNumber, practiceArea, subject, form]); + /* proceeding_type follows appeal_subtype when the user hasn't picked + * one explicitly — extension_request_* always implies 'בל"מ'. */ + useEffect(() => { + if (userTouchedProceeding.current) return; + const proc = appealSubtype.startsWith("extension_request_") ? 'בל"מ' : "ערר"; + if (proc !== form.getValues("proceeding_type")) { + form.setValue("proceeding_type", proc, { shouldValidate: false }); + } + }, [appealSubtype, form]); + const stepIndex = STEPS.findIndex((s) => s.key === step); const isLast = stepIndex === STEPS.length - 1; @@ -162,6 +179,39 @@ export function CaseWizard() {
+
+ + ( + + )} + /> +

+ ערר = הליך עיקרי; בל"מ = בקשה להארכת מועד להגשת ערר +

+
diff --git a/web-ui/src/lib/api/cases.ts b/web-ui/src/lib/api/cases.ts index 0a0db48..44cfa2a 100644 --- a/web-ui/src/lib/api/cases.ts +++ b/web-ui/src/lib/api/cases.ts @@ -54,6 +54,8 @@ export type Case = { respondents?: string[] | null; property_address?: string | null; permit_number?: string | null; + /* 'ערר' = regular appeal, 'בל"מ' = extension-of-time request */ + proceeding_type?: "ערר" | 'בל"מ'; }; export type CaseDocument = { diff --git a/web-ui/src/lib/schemas/case.ts b/web-ui/src/lib/schemas/case.ts index bd1e6ee..9c6c10f 100644 --- a/web-ui/src/lib/schemas/case.ts +++ b/web-ui/src/lib/schemas/case.ts @@ -40,6 +40,15 @@ export const expectedOutcomes = [ { value: "betterment_levy", label: "היטל השבחה" }, ] as const; +/* proceeding_type — distinguishes a regular appeal (ערר) from an + * extension-of-time request (בל"מ). The same case_number can exist as + * both, so this is a separate axis from appeal_subtype/practice_area. */ +export const proceedingTypes = [ + { value: "ערר", label: "ערר" }, + { value: 'בל"מ', label: 'בל"מ' }, +] as const; +export type ProceedingType = (typeof proceedingTypes)[number]["value"]; + export const caseCreateSchema = z.object({ case_number: z .string() @@ -75,6 +84,7 @@ export const caseCreateSchema = z.object({ "extension_request_compensation", "unknown", ] as const satisfies readonly AppealSubtype[]), + proceeding_type: z.enum(["ערר", 'בל"מ'] as const), }); export type CaseCreateInput = z.infer; @@ -100,6 +110,7 @@ export const caseUpdateSchema = z.object({ .optional(), property_address: z.string().trim().max(200).optional(), permit_number: z.string().trim().max(100).optional(), + proceeding_type: z.enum(["ערר", 'בל"מ'] as const).optional(), }); export type CaseUpdateInput = z.infer; diff --git a/web/app.py b/web/app.py index fec332b..8c9302b 100644 --- a/web/app.py +++ b/web/app.py @@ -1234,6 +1234,9 @@ class CaseCreateRequest(BaseModel): # send a domain value explicitly. practice_area: str = "" appeal_subtype: str = "" + # proceeding_type: 'ערר' / 'בל"מ'. Empty → auto-derived from + # appeal_subtype / subject downstream. + proceeding_type: str = "" class CaseUpdateRequest(BaseModel): @@ -1249,6 +1252,7 @@ class CaseUpdateRequest(BaseModel): respondents: list[str] | None = None property_address: str = "" permit_number: str = "" + proceeding_type: str = "" @app.post("/api/cases/create") @@ -1268,6 +1272,7 @@ async def api_case_create(req: CaseCreateRequest): expected_outcome=req.expected_outcome, practice_area=req.practice_area, appeal_subtype=req.appeal_subtype, + proceeding_type=req.proceeding_type, ) parsed = json.loads(result) @@ -1399,6 +1404,7 @@ async def api_case_update(case_number: str, req: CaseUpdateRequest, background_t respondents=req.respondents, property_address=req.property_address, permit_number=req.permit_number, + proceeding_type=req.proceeding_type, ) except ValueError as exc: raise HTTPException(422, str(exc))