215 lines
14 KiB
Markdown
215 lines
14 KiB
Markdown
# הפרדת תחומים משפטיים — `practice_area`
|
||
|
||
> מסמך לצוות פיתוח ה-UI: סיכום השינוי הבק-אנדי שהוכנס ב-`26d09d6` והשלכותיו על קליינטים.
|
||
|
||
**Commit:** `26d09d6` · `main`
|
||
**Repo:** `gitea.nautilus.marcusgroup.org/ezer-mishpati/legal-ai`
|
||
**גודל:** 8 קבצים, +467/-33, קובץ אחד חדש
|
||
|
||
---
|
||
|
||
## הרעיון בשורה אחת
|
||
|
||
הוספנו ציר multi-tenant לדאטה בייס שמפריד תיקים, מסמכים, החלטות וקורפוס סגנון לפי **תחום משפטי** — כדי שחיפוש סמנטי ו-RAG לא יערבבו precedents בין תחומים שונים (היום: רישוי-ובנייה מול היטל-השבחה; בעתיד: גם ביטוח-לאומי, דיני-עבודה).
|
||
|
||
## למה זה חשוב
|
||
|
||
הרכיב המרכזי שמטפל בכתיבת בלוק הדיון (block-writer) שולף "פסקאות תקדים" מקורפוס ההחלטות הקודמות של דפנה תמיר באמצעות חיפוש סמנטי. **לפני השינוי**, שום פילטר תחומי לא הופעל — היה אפשר לקבל פסקה מהחלטה בהיטל השבחה (8xxx) כדוגמה לבלוק דיון של תיק רישוי-ובנייה (1xxx). הכללים, הסעיפים והטון בכל תחום שונים מהותית, וערבוב כזה הוא הזיה משפטית שמסכנת את איכות הטיוטה.
|
||
|
||
באותו אופן: כשנוסיף בעתיד תחומים שונים לחלוטין (ביטוח לאומי, דיני עבודה), קורפוס הסגנון יהפוך מעורב — וללא הציר הזה לא נוכל להבטיח שדוגמאות סגנון נבחרות מהתחום הנכון.
|
||
|
||
---
|
||
|
||
## שני שדות חדשים בכל הטבלאות הרלוונטיות
|
||
|
||
| שדה | טיפוס | ערכים אפשריים | תפקיד |
|
||
|---|---|---|---|
|
||
| `practice_area` | `TEXT` | `appeals_committee` (כיום היחיד) · `national_insurance` · `labor_law` (עתידיים) | תחום-העל. ציר ה-multi-tenancy. כל חיפוש מסונן לפיו. |
|
||
| `appeal_subtype` | `TEXT` | `building_permit` · `betterment_levy` · `compensation_197` · `unknown` | מעדן בתוך תחום. רלוונטי כיום רק ל-`appeals_committee`. |
|
||
|
||
**הסקה אוטומטית ממספר התיק** (קונבנציית ועדת ערר ירושלים):
|
||
|
||
| מספר תיק | `appeal_subtype` |
|
||
|---|---|
|
||
| `1xxx-yy` | `building_permit` |
|
||
| `8xxx-yy` | `betterment_levy` |
|
||
| `9xxx-yy` | `compensation_197` |
|
||
| אחר | `unknown` |
|
||
|
||
המשתמש יכול לעקוף ידנית; אם הוא בחר subtype שלא תואם להסקה האוטומטית, נכתבת רשומת `case_subtype_override` ל-`audit_log` כדי שנוכל לאתר טעויות סיווג.
|
||
|
||
---
|
||
|
||
## שינויים במסד הנתונים (כבר רץ בפרודקשן)
|
||
|
||
`SCHEMA_V4_SQL` ב-`mcp-server/src/legal_mcp/services/db.py` (idempotent — `IF NOT EXISTS` בכל מקום):
|
||
|
||
- שתי עמודות חדשות בכל אחת מ-5 הטבלאות:
|
||
`cases`, `documents`, `document_chunks`, `decisions`, `style_corpus`
|
||
- 4 אינדקסים composite (`practice_area` תמיד ראשון):
|
||
`idx_cases_practice`, `idx_chunks_practice`, `idx_corpus_practice`, `idx_decisions_practice`
|
||
- **Backfill אוטומטי** של כל הרשומות הקיימות לפי regex על `case_number`, ופיזור ל-documents/chunks/decisions לפי FK. רשומות קורפוס היסטוריות (`case_id IS NULL`) מסומנות `appeals_committee`.
|
||
|
||
**אומת אחרי הריצה (פרודקשן):** 0 ערכי NULL ב-`practice_area` בכל 5 הטבלאות (2 cases · 51 documents · 2810 chunks · 1 decision · 24 style_corpus).
|
||
|
||
---
|
||
|
||
## קבצים שהשתנו
|
||
|
||
### חדש
|
||
|
||
| קובץ | תפקיד |
|
||
|---|---|
|
||
| `mcp-server/src/legal_mcp/services/practice_area.py` | מודול מרכזי: `derive_subtype(case_number)`, `validate(area, subtype)`, `is_override(...)`, וקבועי enum (`PRACTICE_AREAS`, `APPEALS_COMMITTEE_SUBTYPES`, `SUBTYPES_BY_AREA`). **כל קוד שצריך להחליט על תחום צריך לעבור דרך המודול הזה — אל תכתבו regex משלכם.** |
|
||
|
||
### Backend — מסד נתונים (`mcp-server/src/legal_mcp/services/db.py`)
|
||
|
||
- `SCHEMA_V4_SQL` חדש + הקריאה אליו ב-`init_schema()`.
|
||
- `create_case(...)` קיבל פרמטרים `practice_area`, `appeal_subtype`.
|
||
- `create_document(...)` ו-`store_chunks(...)` יורשים אוטומטית מה-case (או מקבלים override מפורש לקורפוס אימון שאין לו `case_id`).
|
||
- `create_decision(...)` יורש מה-case.
|
||
- **חדש:** `get_case_practice_area(case_id)` ו-`get_case_practice_area_by_number(case_number)` — שליפה מהירה.
|
||
- `search_similar(...)` ו-`search_similar_paragraphs(...)` קיבלו פרמטרי `practice_area` ו-`appeal_subtype` אופציונליים. הסינון משתמש בעמודה ה-denormalized ב-`document_chunks` ו-`decisions` (ללא JOIN יקר).
|
||
- `add_to_style_corpus(...)` קיבל `practice_area` (ברירת מחדל `appeals_committee`) ו-`appeal_subtype`.
|
||
|
||
### Backend — MCP tools
|
||
|
||
- `mcp-server/src/legal_mcp/tools/cases.py` — `case_create` קיבל `practice_area` + `appeal_subtype`. אם המשתמש סיפק subtype שונה ממה שההסקה האוטומטית אומרת, נכתב `audit_log` עם `action='case_subtype_override'`.
|
||
- `mcp-server/src/legal_mcp/tools/search.py` — `search_decisions` ו-`find_similar_cases` קיבלו פרמטרים `practice_area`, `appeal_subtype`, `case_number`. אם רק `case_number` סופק — ה-`practice_area` נשלף אוטומטית מה-DB.
|
||
- `mcp-server/src/legal_mcp/tools/documents.py` — `document_upload_training` קיבל `practice_area` ו-`appeal_subtype`, עם הסקה אוטומטית מ-`decision_number`.
|
||
- `mcp-server/src/legal_mcp/services/block_writer.py` — **תיקון קריטי** ב-`_build_precedents_context()`: כשבונים את הקשר ה-precedent לבלוק י (דיון), השליפה מסוננת לפי תחום התיק הפעיל. זה סוגר את חור הזיהום שתואר ב"למה זה חשוב".
|
||
|
||
### API ו-UI הקיימים
|
||
|
||
- `web/app.py`:
|
||
- `CaseCreateRequest` (Pydantic) קיבל שני שדות חדשים: `practice_area: str = "appeals_committee"` ו-`appeal_subtype: str = ""`. שניהם **אופציונליים עם defaults** — קליינט קיים לא נשבר.
|
||
- `POST /api/cases/create` מעביר אותם הלאה ל-`case_create` tool.
|
||
- `web/static/index.html`:
|
||
- בויזרד יצירת תיק (step 1) הוספנו שני dropdowns: "תחום משפטי" ו"סוג ערר" — שניהם בתוך אותו `form-row`.
|
||
- JS auto-fill: `wireSubtypeAutofill()` מאזין ל-`input` על `wiz-case-number` ומעדכן את `wiz-appeal-subtype` עד שהמשתמש שינה ידנית את הסלקט.
|
||
- `getWizardData()` מחזיר עכשיו גם `practice_area` ו-`appeal_subtype`.
|
||
- `buildSummary()` ומסך פרטי תיק (`loadCaseView`) מציגים badge כחול עם `practice_area · subtype`.
|
||
- **השדה הישן `committee_type`** הפך ל-`<input type="hidden">` כדי לא לשבור backwards compatibility (Paperclip integration עדיין משתמש בו לבחירת ארגון CMP/CMPA — ראו `web/paperclip_client.py`).
|
||
|
||
---
|
||
|
||
## חוזי API ל-UI חדש
|
||
|
||
### `POST /api/cases/create` — request body
|
||
|
||
```jsonc
|
||
{
|
||
"case_number": "1130-25",
|
||
"title": "...",
|
||
"appellants": ["..."],
|
||
"respondents": ["..."],
|
||
"subject": "",
|
||
"property_address": "",
|
||
"permit_number": "",
|
||
"committee_type": "ועדה מקומית",
|
||
"hearing_date": "",
|
||
"notes": "",
|
||
"expected_outcome": "",
|
||
|
||
// ← חדש (אופציונלי, יש defaults)
|
||
"practice_area": "appeals_committee",
|
||
"appeal_subtype": "building_permit" // ריק = יוסק אוטומטית מ-case_number
|
||
}
|
||
```
|
||
|
||
> **חשוב:** אם תשלחו `appeal_subtype` ריק או לא תשלחו אותו בכלל — השרת יסיק לבד מ-`case_number` (1xxx/8xxx/9xxx). הסקה ידנית בצד הקליינט אופציונלית בלבד (משפרת UX, לא נדרשת לתקינות).
|
||
|
||
### `GET /api/cases/{case_number}/details` — response
|
||
|
||
```jsonc
|
||
{
|
||
"id": "...",
|
||
"case_number": "1130-25",
|
||
// ... כל השדות הקיימים
|
||
"practice_area": "appeals_committee", // ← חדש
|
||
"appeal_subtype": "building_permit" // ← חדש
|
||
}
|
||
```
|
||
|
||
`list_cases` ו-`case_get` מחזירים את אותם שדות.
|
||
|
||
### Enums לסנכרון בקליינט
|
||
|
||
```typescript
|
||
type PracticeArea = 'appeals_committee' | 'national_insurance' | 'labor_law';
|
||
type AppealSubtype = 'building_permit' | 'betterment_levy' | 'compensation_197' | 'unknown';
|
||
|
||
const PRACTICE_AREA_LABELS: Record<PracticeArea, string> = {
|
||
appeals_committee: 'ועדת ערר',
|
||
national_insurance: 'ביטוח לאומי',
|
||
labor_law: 'דיני עבודה',
|
||
};
|
||
|
||
const SUBTYPE_LABELS: Record<AppealSubtype, string> = {
|
||
building_permit: 'רישוי ובנייה',
|
||
betterment_levy: 'היטל השבחה',
|
||
compensation_197: "פיצויים (ס' 197)",
|
||
unknown: 'לא ידוע',
|
||
};
|
||
|
||
// 1xxx → building_permit · 8xxx → betterment_levy · 9xxx → compensation_197
|
||
function deriveSubtypeFromCaseNumber(caseNumber: string): AppealSubtype {
|
||
const m = caseNumber.trim().match(/^(\d)/);
|
||
if (!m) return 'unknown';
|
||
return ({ '1': 'building_permit', '8': 'betterment_levy', '9': 'compensation_197' } as const)[m[1]] ?? 'unknown';
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## מה ה-UI החדש חייב לכלול
|
||
|
||
1. **טופס יצירת תיק** — שדה `practice_area` (היום ועדת ערר בלבד, אבל בנו את הקומפוננטה כ-dropdown מוכן להרחבה) + שדה `appeal_subtype` עם auto-fill ממספר התיק. אפשרות override ידני.
|
||
2. **תצוגת תיק / רשימת תיקים** — badge גלוי שמראה את התחום + סוג הערר. קריטי כדי שמשתמשים יבינו באיזה הקשר הם עובדים, במיוחד כשנוסיף תחומים נוספים.
|
||
3. **פילטרים ברשימות חיפוש/תיקים** — הוסיפו אופציה לסנן לפי תחום (אופציונלית בשלב זה, חובה ברגע שנוסיף תחום שני).
|
||
4. **קריאות לחיפוש** (`search_decisions`, `find_similar_cases`) — אם אתם מבצעים חיפוש בהקשר של תיק, **תמיד תעבירו `case_number`** — השרת יוסיף את הסינון אוטומטית. אחרת תקבלו warning ב-log על חיפוש חוצה-תחומים.
|
||
|
||
## תאימות לאחור
|
||
|
||
**אין שבירות חוזה.** כל השדות החדשים אופציונליים עם defaults. קליינט שלא מכיר את `practice_area`/`appeal_subtype` ממשיך לעבוד — פשוט יקבל ערכי ברירת המחדל (`appeals_committee` + מה שיוסק ממספר התיק).
|
||
|
||
**שינוי התנהגות אחד שכדאי לדעת:**
|
||
`committee_type` נשאר בסכמה ובחוזה אבל ב-UI הקיים הוא הפך ל-hidden field. אם ה-UI החדש שלכם מאפשר עריכה ידנית של `committee_type`, ודאו שהעדכון לא דורס את הסמנטיקה החדשה. עדיף לחשוב על:
|
||
- `committee_type` — "מצב הוועדה במקור" (legacy + Paperclip routing)
|
||
- `practice_area` / `appeal_subtype` — "איך המערכת מסווגת את התיק לצורך RAG/חיפוש"
|
||
|
||
---
|
||
|
||
## בדיקות שעברו
|
||
|
||
- Unit tests של `practice_area.py` (`derive_subtype`, `validate`, `is_override`)
|
||
- Migration רץ על פרודקשן: 0 NULLs ב-5 הטבלאות
|
||
- `case_create('1999-99')` → אוטומטית `building_permit`
|
||
- `case_create('8999-99', appeal_subtype='building_permit')` → audit log רושם `case_subtype_override`
|
||
- `search_similar(practice_area='national_insurance')` → 0 תוצאות (אין leakage לתחום ריק)
|
||
- `search_similar(practice_area='appeals_committee')` → 5 תוצאות תקינות
|
||
|
||
---
|
||
|
||
## שאלות פתוחות לצוות UI
|
||
|
||
1. **Backfill ל-Paperclip routing:** היום `paperclip_client.py` ממפה לארגון (CMP/CMPA) לפי `committee_type` המילולי. רוצים שנעביר את ה-routing להסתמך על `appeal_subtype` כדי שיהיה מקור-אמת אחד?
|
||
2. **פילטר תחום ברשימות חיפוש גלובליות:** האם ה-UI החדש יציג את התחום כ-segmented control בראש כל מסך חיפוש, או כ-filter chip?
|
||
3. **תחומים עתידיים:** האם יש כבר תכנון לאיזה תחום שני נוסיף ראשון (ביטוח לאומי / דיני עבודה / אחר)? אם כן — נוכל להכין את ה-`SUBTYPES_BY_AREA` מראש ב-`practice_area.py`.
|
||
|
||
---
|
||
|
||
## נקודות מגע בקוד (להפניית קריאה מהירה)
|
||
|
||
| תחום | קובץ | פונקציה / סימן |
|
||
|---|---|---|
|
||
| מודול דרייב/ולידציה | `mcp-server/src/legal_mcp/services/practice_area.py` | `derive_subtype`, `validate`, `is_override` |
|
||
| סכמה ומיגרציה | `mcp-server/src/legal_mcp/services/db.py` | `SCHEMA_V4_SQL`, `init_schema` |
|
||
| יצירת תיק | `mcp-server/src/legal_mcp/services/db.py` · `mcp-server/src/legal_mcp/tools/cases.py` | `create_case`, `case_create` |
|
||
| הורשה למסמכים/chunks | `mcp-server/src/legal_mcp/services/db.py` | `create_document`, `store_chunks`, `create_decision` |
|
||
| חיפוש מסונן | `mcp-server/src/legal_mcp/services/db.py` · `mcp-server/src/legal_mcp/tools/search.py` | `search_similar`, `search_similar_paragraphs`, `search_decisions`, `find_similar_cases` |
|
||
| תיקון contamination בבלוק דיון | `mcp-server/src/legal_mcp/services/block_writer.py` | `_build_precedents_context` |
|
||
| קורפוס אימון | `mcp-server/src/legal_mcp/tools/documents.py` · `mcp-server/src/legal_mcp/services/db.py` | `document_upload_training`, `add_to_style_corpus` |
|
||
| API חוזה | `web/app.py` | `CaseCreateRequest`, `api_case_create` |
|
||
| UI קיים | `web/static/index.html` | `wireSubtypeAutofill`, `getWizardData`, `buildSummary`, `loadCaseView` |
|