fix(retrieval): make decisions findable by name + unhide committee uploads
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 3m57s
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 3m57s
Root cause of "agent can't find the Agasi decision in the corpus" (CMPA-55): the decision was fully ingested, but the retrieval layer failed on the realistic agent query — searching by case name. - RC-A (#52): lexical tsvector covered only chunk content + halacha text, so a bare-name query ("אגסי") matched decisions that *cite* the case, not the case itself. Add meta_tsv on case_law(case_name, case_number) (SCHEMA V20) and OR it into the lexical halacha/chunk SQL with a match boost, so a name/number hit surfaces the case's own rows. Agasi: rank 4 → rank 1. - RC-B (#53): precedent_library_list hard-defaulted source_kind=external_upload and never exposed the param, hiding uploaded ערר/בל"מ (internal_committee) decisions. Thread source_kind through service → tool → MCP tool (supports 'internal_committee' / 'all_committees'). - #54: agent instructions (researcher/analyst/writer) — search-by-name protocol: add content/case-number, search both corpora, use all_committees before declaring "not in corpus". - #55: chunker produced tiny fragment chunks ("דיון", "החלטה") from header keywords matched mid-sentence. Anchor SECTION_PATTERNS to line start + merge sub-min sections; exclude <50-char fragments at query time (484 existing fragments hidden; full re-chunk tracked as #57). Tests: scripts/test_retrieval_by_name.py (name ranks case above citer + substantive regressions); chunker unit checks (0 tiny chunks). New findings filed as tasks #56 (halacha source_kind leak) and #57 (re-chunk migration). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -455,12 +455,12 @@ X שאלות עומדות להכרעה:
|
||||
### 8א. אימות פסיקה
|
||||
סרוק את עמדות היו"ר וזהה כל אזכור פסיקה (בג"ץ, עע"מ, עת"מ, ע"א, ערר וכו').
|
||||
לכל פסק דין שמוזכר:
|
||||
1. חפש ב**קורפוס הסמכותי** (`search_precedent_library`) — חובה ראשונה. שם נמצאות הלכות מאושרות עם supporting_quote מוכן לציטוט.
|
||||
1. חפש ב**קורפוס הסמכותי** (`search_precedent_library`) — חובה ראשונה. שם נמצאות הלכות מאושרות עם supporting_quote מוכן לציטוט. הקורפוס כולל גם הלכות מהחלטות ועדות ערר שהועלו (internal_committee).
|
||||
2. חפש בקאנון דפנה (`search_decisions`, `find_similar_cases`)
|
||||
3. חפש במסמכי התיק (`search_case_documents`) — אולי מצוטט בכתבי הטענות
|
||||
4. **אם נמצא ב-precedent_library** — צטט citation+supporting_quote מדויקים מהקורפוס.
|
||||
5. **אם נמצא רק במסמכי התיק** — סמן: "מקור: כתבי טענות, דורש אימות מול הקורפוס".
|
||||
6. **אם לא נמצא בכלל** — סמן: "דורש אימות חיצוני" + נסח הנחיות חיפוש.
|
||||
6. **אם לא נמצא בכלל** — קודם **נסה שוב עם הקשר** (לא שם לבדו): צרף מונחי תוכן או מספר תיק לשאילתה. שם תיק לבדו (`"אגסי"`) אינו מפתח אמין — הוא עלול להחזיר את מי שמצטט את התיק ולא את התיק עצמו. רק אם גם זה ריק — סמן: "דורש אימות חיצוני" + נסח הנחיות חיפוש.
|
||||
|
||||
הוסף לסעיף "7א. שאילתות לקורפוסים" כל query נוסף שהורצה ב-pass 2.
|
||||
|
||||
|
||||
@@ -269,9 +269,18 @@ search_internal_decisions(
|
||||
|
||||
**מינימום:** queries לקורפוס הסמכותי = מספר סוגיות מרכזיות שזוהו.
|
||||
|
||||
#### 2ב.4א — איתור החלטה ספציפית לפי שם — פרוטוקול לפני "לא בקורפוס" ⚠️
|
||||
|
||||
שם תיק לבדו (למשל `"אגסי"`) **אינו מפתח חיפוש אמין**. ההטמעה הסמנטית והאינדקס הלקסיקלי בנויים על תוכן ההלכה/הפסקה — כך ששאילתת-שם עלולה להחזיר דווקא החלטות ש**מצטטות** את התיק, ולא את התיק עצמו. לפני שמכריזים שהחלטה אינה בקורפוס:
|
||||
|
||||
1. **הוסף הקשר לשאילתה** — לא `"אגסי"` אלא `"אגסי פטור 19(ג)(1) שתי דירות 140 מ"ר"`, או חפש לפי **מספר התיק** (`"ערר 81002-01-21"`).
|
||||
2. **חפש בשני הקורפוסים** — `search_precedent_library` **וגם** `search_internal_decisions`. החלטות ערר/בל"מ שהיו"ר מעלה נשמרות כ-`internal_committee` ומתגלות בחיפוש הפנימי.
|
||||
3. **לאימות קיום / דפדוף** — `precedent_library_list(search="<שם>", source_kind="all_committees")`. ברירת המחדל `external_upload` **מסתירה** החלטות ועדת ערר שהועלו — חובה `all_committees` או `internal_committee`.
|
||||
4. רק אם **כל** הניסיונות לעיל ריקים — הכרז "לא בקורפוס" ועבור ל-2ב.5.
|
||||
|
||||
#### 2ב.5 — תיעוד פסיקה חסרה (`missing_precedent_create`) — חובה
|
||||
|
||||
**מתי לקרוא:** לכל ציטוט שהצדדים הביאו (בכתב ערר / תגובה / תגובת ועדה) **שלא נמצא בקורפוס** אחרי חיפוש מובנה (`search_precedent_library` + `search_internal_decisions` + `precedent_search_library`).
|
||||
**מתי לקרוא:** לכל ציטוט שהצדדים הביאו (בכתב ערר / תגובה / תגובת ועדה) **שלא נמצא בקורפוס** אחרי חיפוש מובנה לפי פרוטוקול 2ב.4א (`search_precedent_library` + `search_internal_decisions` + `precedent_search_library`, כולל שאילתה עם הקשר/מספר תיק).
|
||||
|
||||
**למה זה חשוב:**
|
||||
- ה-writer יודע שלא להסתמך על פסיקה שלא ב-DB ("טוענים שמופיע" ≠ "אומת")
|
||||
|
||||
@@ -351,6 +351,8 @@ fi
|
||||
|
||||
חפש לפי `practice_area` (rishuy_uvniya / betterment_levy / compensation_197) ולפי `subject_tag` רלוונטי. הלכות שלא אושרו ע"י דפנה לא מוחזרות מהכלי — אם החיפוש ריק, חזור ל-`search_decisions` בלבד.
|
||||
|
||||
**איתור החלטה לפי שם:** אם אתה מחפש החלטה ספציפית בשמה (למשל "אגסי"), אל תחפש בשם לבדו — צרף מונחי תוכן או מספר תיק (`"אגסי 19(ג)(1) 140 מ"ר"` / `"ערר 81002-01-21"`). שאילתת-שם בלבד עלולה להחזיר את מי שמצטט את ההחלטה ולא את ההחלטה עצמה.
|
||||
|
||||
### ⚠️ ניסוח ציטוטי פסיקה בקול ההחלטה — לפי `source_kind`
|
||||
|
||||
כל רשומה בקורפוס נושאת `source_kind` (ראה בפלט של `precedent_library_get` / `search_precedent_library` / `search_internal_decisions`). הניסוח בבלוק י **משתנה לפי הסוג** — לא רק הציטוט, אלא **התפקיד הרטורי** של פסק הדין בהנמקה:
|
||||
|
||||
@@ -1388,50 +1388,59 @@
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Audit + migration practice_area (1xxx→rishuy_uvniya, 8xxx→betterment_levy, 9xxx→compensation_197)",
|
||||
"status": "done"
|
||||
"status": "done",
|
||||
"parentId": "undefined"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"title": "Audit + reclassify case_law source_kind external_upload → internal_committee עבור 'ערר' prefix",
|
||||
"status": "done"
|
||||
"status": "done",
|
||||
"parentId": "undefined"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"title": "Delete + re-extract halachot עבור רשומות שעברו reclassification",
|
||||
"status": "done"
|
||||
"status": "done",
|
||||
"parentId": "undefined"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"title": "תיקון נתיב יצירת תיק לתיוג practice_area נכון מההתחלה",
|
||||
"status": "done"
|
||||
"status": "done",
|
||||
"parentId": "undefined"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"title": "תיקון /api/precedent-library/upload לניתוב לפי תחילית הציטוט",
|
||||
"status": "done"
|
||||
"status": "done",
|
||||
"parentId": "undefined"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"title": "מבחני רגרסיה לכל 3 הbaגים",
|
||||
"status": "done"
|
||||
"status": "done",
|
||||
"parentId": "undefined"
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"title": "תיקון MCP `case_update` + API `PUT /api/cases/{case_number}` לתמוך בעדכון practice_area + appeal_subtype",
|
||||
"status": "done",
|
||||
"details": "התגלה ב-26/05/2026: MCP tool case_update והAPI לא מקבלים את השדה practice_area, ולכן אי-אפשר לתקן תיוג שגוי דרך הממשק. נאלצתי לעדכן ידנית ב-SQL. צריך להוסיף את השדות ל-CaseUpdateRequest ב-web/app.py וב-cases_tools.case_update בmcp-server."
|
||||
"details": "התגלה ב-26/05/2026: MCP tool case_update והAPI לא מקבלים את השדה practice_area, ולכן אי-אפשר לתקן תיוג שגוי דרך הממשק. נאלצתי לעדכן ידנית ב-SQL. צריך להוסיף את השדות ל-CaseUpdateRequest ב-web/app.py וב-cases_tools.case_update בmcp-server.",
|
||||
"parentId": "undefined"
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"title": "[prevention] DB CHECK constraints: source_kind='internal_committee' ⇒ chair_name NOT NULL; cases.practice_area enum",
|
||||
"status": "done",
|
||||
"description": "מיגרציה: ALTER TABLE case_law ADD CONSTRAINT chair_required_for_internal CHECK (source_kind <> 'internal_committee' OR (chair_name IS NOT NULL AND chair_name <> '')); וכן CHECK על cases.practice_area לערכים תקינים. חייב לרוץ אחרי subtask #2 (backfill) אחרת constraint creation ייכשל."
|
||||
"description": "מיגרציה: ALTER TABLE case_law ADD CONSTRAINT chair_required_for_internal CHECK (source_kind <> 'internal_committee' OR (chair_name IS NOT NULL AND chair_name <> '')); וכן CHECK על cases.practice_area לערכים תקינים. חייב לרוץ אחרי subtask #2 (backfill) אחרת constraint creation ייכשל.",
|
||||
"parentId": "undefined"
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"title": "[prevention] Unify practice_area taxonomy — מיפוי או מחיקה של appeals_committee מ-practice_area.py",
|
||||
"status": "done",
|
||||
"description": "ב-mcp-server/src/legal_mcp/services/practice_area.py:21 יש PRACTICE_AREAS={appeals_committee,national_insurance,labor_law} שסותר את ה-DB constraint של case_law (rishuy_uvniya/betterment_levy/compensation_197). grep מקיף לכל caller של 'appeals_committee'; להחליף במיפוי מפורש או למחוק."
|
||||
"description": "ב-mcp-server/src/legal_mcp/services/practice_area.py:21 יש PRACTICE_AREAS={appeals_committee,national_insurance,labor_law} שסותר את ה-DB constraint של case_law (rishuy_uvniya/betterment_levy/compensation_197). grep מקיף לכל caller של 'appeals_committee'; להחליף במיפוי מפורש או למחוק.",
|
||||
"parentId": "undefined"
|
||||
}
|
||||
],
|
||||
"updatedAt": "2026-05-26T08:35:22.762800Z"
|
||||
@@ -1452,19 +1461,22 @@
|
||||
"id": 1,
|
||||
"title": "Backfill chair_name + district לכל 7 הרשומות החסרות (LLM extraction)",
|
||||
"status": "done",
|
||||
"description": "psql query: SELECT id, case_number FROM case_law WHERE source_kind='internal_committee' AND (chair_name IS NULL OR chair_name=''); לכל אחת — חילוץ ע\"י precedent_metadata_extractor.extract_and_apply."
|
||||
"description": "psql query: SELECT id, case_number FROM case_law WHERE source_kind='internal_committee' AND (chair_name IS NULL OR chair_name=''); לכל אחת — חילוץ ע\"י precedent_metadata_extractor.extract_and_apply.",
|
||||
"parentId": "undefined"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"title": "[prevention] Validation: chair_name+district required ב-internal_decisions_upload (API+MCP)",
|
||||
"status": "done",
|
||||
"description": "ב-web/app.py:4607-4680 כיום chair_name/district = Form(\"\") (default ריק). שנה ל-required עם validation שדוחה ריק כשsource_kind='internal_committee'. הוסף enum של 6 ערכי district (ירושלים/מרכז/תל אביב/צפון/דרום/ארצי)."
|
||||
"description": "ב-web/app.py:4607-4680 כיום chair_name/district = Form(\"\") (default ריק). שנה ל-required עם validation שדוחה ריק כשsource_kind='internal_committee'. הוסף enum של 6 ערכי district (ירושלים/מרכז/תל אביב/צפון/דרום/ארצי).",
|
||||
"parentId": "undefined"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"title": "[prevention] UI dropdown ל-district בטופס העלאת החלטות ועדה (web-ui)",
|
||||
"status": "done",
|
||||
"description": "במקום free-text — Select של 6 הערכים. גם בטופס חיפוש (search_internal_decisions)."
|
||||
"description": "במקום free-text — Select של 6 הערכים. גם בטופס חיפוש (search_internal_decisions).",
|
||||
"parentId": "undefined"
|
||||
}
|
||||
],
|
||||
"updatedAt": "2026-05-26T08:35:22.762800Z"
|
||||
@@ -1521,32 +1533,38 @@
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Migration + model missing_precedents",
|
||||
"status": "done"
|
||||
"status": "done",
|
||||
"parentId": "undefined"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"title": "API endpoints POST/GET/upload/PATCH /api/missing-precedents",
|
||||
"status": "done"
|
||||
"status": "done",
|
||||
"parentId": "undefined"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"title": "MCP tools missing_precedent_create/list/close",
|
||||
"status": "done"
|
||||
"status": "done",
|
||||
"parentId": "undefined"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"title": "Next.js page /missing-precedents עם list + detail + upload form",
|
||||
"status": "done"
|
||||
"status": "done",
|
||||
"parentId": "undefined"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"title": "Auto-creation hook במחקר (legal-researcher יוצר רשומה כשמזהה ציטוט חסר)",
|
||||
"status": "done"
|
||||
"status": "done",
|
||||
"parentId": "undefined"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"title": "Webhook עדכון לפלאגין Paperclip + Comment לחיים",
|
||||
"status": "done"
|
||||
"status": "done",
|
||||
"parentId": "undefined"
|
||||
}
|
||||
],
|
||||
"updatedAt": "2026-05-26T08:35:22.762800Z"
|
||||
@@ -1564,27 +1582,32 @@
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Migration + models legal_arguments + legal_argument_propositions",
|
||||
"status": "done"
|
||||
"status": "done",
|
||||
"parentId": "undefined"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"title": "LLM aggregation job (Hermes/DeepSeek profile)",
|
||||
"status": "done"
|
||||
"status": "done",
|
||||
"parentId": "undefined"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"title": "API + MCP tool aggregate_claims_to_arguments",
|
||||
"status": "done"
|
||||
"status": "done",
|
||||
"parentId": "undefined"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"title": "UI display update — case detail page מציג טיעונים אמיתיים",
|
||||
"status": "done"
|
||||
"status": "done",
|
||||
"parentId": "undefined"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"title": "Backfill לכל התיקים הקיימים",
|
||||
"status": "done"
|
||||
"status": "done",
|
||||
"parentId": "undefined"
|
||||
}
|
||||
],
|
||||
"updatedAt": "2026-05-26T08:35:22.762800Z"
|
||||
@@ -1602,27 +1625,32 @@
|
||||
{
|
||||
"id": 1,
|
||||
"title": "הוספת 3 ערכי enum ל-practice_area.py APPEALS_COMMITTEE_SUBTYPES",
|
||||
"status": "done"
|
||||
"status": "done",
|
||||
"parentId": "undefined"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"title": "כתיבת 3 templates מתודולוגיים ב-docs/methodology/extension-request-{type}.md",
|
||||
"status": "done"
|
||||
"status": "done",
|
||||
"parentId": "undefined"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"title": "אוטו-זיהוי בקוד יצירת תיק (subject='בקשה להארכת מועד' → קביעת subtype לפי practice_area)",
|
||||
"status": "done"
|
||||
"status": "done",
|
||||
"parentId": "undefined"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"title": "UI badge + filter ייעודי לבל\"מ",
|
||||
"status": "done"
|
||||
"status": "done",
|
||||
"parentId": "undefined"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"title": "עדכון web/paperclip_client.py mapping ל-company עבור 3 הערכים החדשים",
|
||||
"status": "done"
|
||||
"status": "done",
|
||||
"parentId": "undefined"
|
||||
}
|
||||
],
|
||||
"updatedAt": "2026-05-26T08:35:22.762800Z"
|
||||
@@ -1647,55 +1675,65 @@
|
||||
{
|
||||
"id": 1,
|
||||
"title": "עדכון .claude/agents/legal-ceo.md — routing + statuses + wake reasons",
|
||||
"status": "done"
|
||||
"status": "done",
|
||||
"parentId": "undefined"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"title": "עדכון .claude/agents/legal-analyst.md — practice_area, legal_arguments, בל\"מ detection",
|
||||
"status": "done"
|
||||
"status": "done",
|
||||
"parentId": "undefined"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"title": "עדכון .claude/agents/legal-researcher.md — 2 layers, missing_precedents, citations, בל\"מ templates",
|
||||
"status": "done"
|
||||
"status": "done",
|
||||
"parentId": "undefined"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"title": "עדכון .claude/agents/legal-writer.md — legal_arguments view, בל\"מ templates",
|
||||
"status": "done"
|
||||
"status": "done",
|
||||
"parentId": "undefined"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"title": "עדכון .claude/agents/legal-qa.md — בל\"מ-aware validation",
|
||||
"status": "done"
|
||||
"status": "done",
|
||||
"parentId": "undefined"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"title": "עדכון .claude/agents/HEARTBEAT.md — כללי routing משותפים + research_complete status",
|
||||
"status": "done"
|
||||
"status": "done",
|
||||
"parentId": "undefined"
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"title": "סנכרון לכל החברות CMPA mirror — sync_agents_across_companies.py",
|
||||
"status": "done"
|
||||
"status": "done",
|
||||
"parentId": "undefined"
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"title": "[alignment] researcher docs: דרישה מפורשת שכל 'ערר' → internal_decision_upload, לעולם לא precedent_library_upload",
|
||||
"status": "done",
|
||||
"description": "בלגל-researcher.md: דוגמת קוד מפורשת + flowchart החלטה: לפי תחילית הציטוט. הסבר על השלילה של precedent_library_upload כשמדובר ב-ערר. תלוי במשימה #39 (MCP tool חדש)."
|
||||
"description": "בלגל-researcher.md: דוגמת קוד מפורשת + flowchart החלטה: לפי תחילית הציטוט. הסבר על השלילה של precedent_library_upload כשמדובר ב-ערר. תלוי במשימה #39 (MCP tool חדש).",
|
||||
"parentId": "undefined"
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"title": "[alignment] analyst docs: הסבר על 2 taxonomies של practice_area + מתי משתמשים בכל אחת",
|
||||
"status": "done",
|
||||
"description": "בלגל-analyst.md: טבלה ברורה — practice_area (case_law) vs practice_area (cases). מתי להעביר rishuy_uvniya ומתי appeals_committee. אחרי משימה #30.9 (taxonomy unification) — סביר שזה ייפשט."
|
||||
"description": "בלגל-analyst.md: טבלה ברורה — practice_area (case_law) vs practice_area (cases). מתי להעביר rishuy_uvniya ומתי appeals_committee. אחרי משימה #30.9 (taxonomy unification) — סביר שזה ייפשט.",
|
||||
"parentId": "undefined"
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"title": "[alignment] writer docs: הבחנה בין source_kind בציטוט (binding vs persuasive)",
|
||||
"status": "done",
|
||||
"description": "בלגל-writer.md: 'החלטת ועדת ערר אחרת ⇒ עקביות אופקית, לא הלכה מחייבת'. 'פס\"ד עליון/מנהלי ⇒ סמכותי בינדינג'. דוגמאות פרזיולוגיה מ-skills/decision/SKILL.md."
|
||||
"description": "בלגל-writer.md: 'החלטת ועדת ערר אחרת ⇒ עקביות אופקית, לא הלכה מחייבת'. 'פס\"ד עליון/מנהלי ⇒ סמכותי בינדינג'. דוגמאות פרזיולוגיה מ-skills/decision/SKILL.md.",
|
||||
"parentId": "undefined"
|
||||
}
|
||||
],
|
||||
"updatedAt": "2026-05-26T07:41:47.880478Z"
|
||||
@@ -1870,17 +1908,96 @@
|
||||
"priority": "low",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-05-26T11:27:09.039154Z"
|
||||
},
|
||||
{
|
||||
"id": "52",
|
||||
"title": "[Retrieval RC-A] הוספת case_name + case_number ל-tsvector הלקסיקלי",
|
||||
"description": "השורש האמיתי לכך שסוכן לא מאתר החלטה לפי שם (אגסי). ה-tsvector הלקסיקלי (SCHEMA_V12_SQL ב-db.py) בנוי רק מ-precedent_chunks.content ומ-halachot rule/quote/reasoning — לא משם התיק/הצד או ממספר התיק. לכן שאילתת-שם מחזירה את מי שמצטט את ההחלטה, לא את ההחלטה עצמה. לשלב את case_law.case_name + case_number באינדקס הלקסיקלי (tsvector ייעודי על case_law או setweight) כך שחיפוש לפי שם יפגע ברשומה עצמה.",
|
||||
"status": "done",
|
||||
"priority": "high",
|
||||
"dependencies": [],
|
||||
"details": "קבצים: mcp-server/src/legal_mcp/services/db.py (SCHEMA_V12_SQL ~שורה 774, search_precedent_library_lexical), hybrid_search.py (_merge_sem_lex). דורש ALTER TABLE + migration על Postgres (localhost:5433) + restart MCP server. בדיקה: search_internal_decisions('אגסי') ו-search_precedent_library('אגסי') חייבים להחזיר את אגסי (1a87efe5) בעמוד הראשון.",
|
||||
"testStrategy": "reproduction test: query='אגסי' → expect case_law_id 1a87efe5 in top-3. regression: substantive query עדיין מחזיר 0.6+ score.",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-05-30T11:05:36.307Z"
|
||||
},
|
||||
{
|
||||
"id": "53",
|
||||
"title": "[Retrieval RC-B] חיפוש/רשימה מאוחדים — לא לחתוך internal_committee",
|
||||
"description": "החלטות ערר/בל\"מ שמועלות נשמרות source_kind='internal_committee'. precedent_library_list ברירת מחדל external_upload ומסתיר אותן; כלי ה-MCP precedent_library_list אפילו לא חושף פרמטר source_kind, כך שסוכן לעולם לא יכול לדפדף בהן. לחשוף source_kind/all_committees בכלי ה-MCP ובמידת הצורך לאחד את שכבת ה-list/search.",
|
||||
"status": "done",
|
||||
"priority": "high",
|
||||
"dependencies": [
|
||||
"52"
|
||||
],
|
||||
"details": "קבצים: web/app.py (precedent_library_list ~5194, all_committees expansion ב-db.list_external_case_law ~2708), mcp-server tool def ל-precedent_library_list. בדיקה: precedent_library_list יכול להחזיר את אגסי כשמבקשים committees; חיפוש סמנטי כבר מאוחד (אומת).",
|
||||
"testStrategy": "precedent_library_list(source_kind='all_committees', practice_area='betterment_levy') כולל את אגסי+וינפלד. regression: ברירת מחדל external_upload עדיין מחזירה 14 ולא שוברת UI.",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-05-30T11:09:44.511Z"
|
||||
},
|
||||
{
|
||||
"id": "54",
|
||||
"title": "[Retrieval RC-3] הנחיית סוכנים — איתור לפי שם + שני קורפוסים",
|
||||
"description": "לעדכן הנחיות legal-analyst/researcher/writer: לאיתור החלטה ספציפית לפי שם להוסיף מונחי תוכן או מספר תיק, ולחפש בשני הקורפוסים (search_internal_decisions + search_precedent_library) לפני שמסיקים 'לא קיים בקורפוס'. כולל יצירת missing_precedent רק אחרי חיפוש כפול.",
|
||||
"status": "done",
|
||||
"priority": "medium",
|
||||
"dependencies": [
|
||||
"53"
|
||||
],
|
||||
"details": "קבצים: .claude/agents/legal-analyst.md, legal-researcher.md, legal-writer.md. אחרי שינוי skills/agent config — להריץ sync_agents_across_companies.py.",
|
||||
"testStrategy": "קריאת ההנחיות מאשרת fallback ברור; (אם אפשר) הרצת סוכן על שאילתת-שם מחזירה את ההחלטה.",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-05-30T11:12:44.727Z"
|
||||
},
|
||||
{
|
||||
"id": "55",
|
||||
"title": "[Retrieval RC-4] תיקון chunking — פרגמנטים זעירים",
|
||||
"description": "בתוצאות החיפוש מופיעים chunks של מילה-שתיים ('דיון','דיון וב','סיכום ו') כתוצאות מובילות. מציפים תוצאות ומורידים דירוג תוכן אמיתי. לחקור את chunker.py (פיצול לפי כותרת-סעיף שיוצר chunks ריקים) ולתקן: מינימום אורך chunk / מיזוג כותרת לגוף.",
|
||||
"status": "done",
|
||||
"priority": "medium",
|
||||
"dependencies": [
|
||||
"54"
|
||||
],
|
||||
"details": "קבצים: mcp-server/src/legal_mcp/services/chunker.py (SECTION_PATTERNS). דורש שיקול re-chunk לרשומות קיימות — לבדוק עלות מול feedback_no_reocr_retrofit (להשתמש בטקסט שמור, לא re-OCR).",
|
||||
"testStrategy": "אין chunks < N תווים בקורפוס אחרי תיקון; search_internal_decisions('אגסי') לא מציג פרגמנטי 'דיון'.",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-05-30T11:19:23.923Z"
|
||||
},
|
||||
{
|
||||
"id": "56",
|
||||
"title": "[Retrieval finding] halacha_filters לא מסננים source_kind — דליפה חוצת-קורפוסים",
|
||||
"description": "התגלה תוך כדי משימה 53. ב-search_precedent_library_semantic וב-search_precedent_library_lexical (db.py): chunk_filters כוללים cl.source_kind=$sk אבל halacha_filters כוללים רק review_status. תוצאה: search_precedent_library(external) מחזיר גם הלכות internal_committee, ו-search_internal_decisions(internal) מחזיר גם הלכות external. אי-עקביות: chunks מסוננים, halachot לא. כרגע זה דווקא מסייע למציאוּת (לכן לא רגרסיה), אבל לא עקבי. דורש החלטת מדיניות: או לסנן halachot גם לפי source_kind (עקבי, אך 'מסתיר' שכבות), או להשאיר מאוחד במכוון + לתעד. אם משאירים מאוחד — לעדכן docstrings של שני הכלים שזה לא 'corpus נפרד'.",
|
||||
"status": "pending",
|
||||
"priority": "low",
|
||||
"dependencies": [],
|
||||
"details": "db.py: search_precedent_library_semantic (~שורה הקודמת ל-3311), search_precedent_library_lexical (3346). שתי הפונקציות: halacha_filters=['h.review_status IN ...'] — חסר cl.source_kind. נמצא בעת בדיקת רגרסיה למשימה 53.",
|
||||
"testStrategy": "לאחר החלטה: אם מסננים — search_precedent_library('...substantive...', external) לא מחזיר case_law_id internal; אם משאירים — docstring מעודכן + טסט מאשר התנהגות מכוונת.",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-05-30T11:09:30.257989+00:00"
|
||||
},
|
||||
{
|
||||
"id": "57",
|
||||
"title": "[Retrieval #55 follow-up] re-chunk+re-embed של פסיקה שהוטמעה לפני תיקון ה-chunker",
|
||||
"description": "משימה 55 תיקנה את ה-chunker (עיגון כותרות + מיזוג) ומסננת את 484 הפרגמנטים בזמן query. הרמדיאציה המלאה: re-chunk מ-full_text השמור (ללא re-OCR — תואם feedback_no_reocr_retrofit) + re-embed, כדי שהתוכן יהיה נכון ולא רק מוסתר. נדחה כי זו מיגרציית-נתונים עם עלות Voyage API על ~13+ תיקים — דורש אישור עלות מ-chaim לפני הרצה. לבדוק כמה תיקים מושפעים (יש להם chunk<50) ולהריץ בקבוצות.",
|
||||
"status": "pending",
|
||||
"priority": "low",
|
||||
"dependencies": [
|
||||
"55"
|
||||
],
|
||||
"details": "מקור: case_law.full_text קיים. נתיב: chunker.chunk_document(_hierarchical) → embeddings → החלפת precedent_chunks לתיק. למחוק chunks ישנים של התיק לפני הוספה. אחרי הרצה — ניתן להסיר את פילטר ה->=50 query (אופציונלי). תיקים מושפעים: SELECT DISTINCT case_law_id WHERE length(trim(content))<50.",
|
||||
"testStrategy": "אחרי re-chunk לתיק לדוגמה: 0 chunks<50 לאותו case_law_id; search_internal_decisions עדיין מחזיר את התיק; ספירת chunks סבירה.",
|
||||
"subtasks": [],
|
||||
"updatedAt": "2026-05-30T11:19:06.142606+00:00"
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"version": "1.0.0",
|
||||
"lastModified": "2026-05-04T17:29:25.687Z",
|
||||
"taskCount": 29,
|
||||
"completedCount": 24,
|
||||
"lastModified": "2026-05-30T11:19:23.923Z",
|
||||
"taskCount": 57,
|
||||
"completedCount": 52,
|
||||
"tags": [
|
||||
"legal-ai"
|
||||
],
|
||||
"updated": "2026-05-26T06:39:31.733370"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -201,11 +201,20 @@ async def precedent_library_list(
|
||||
precedent_level: str = "",
|
||||
source_type: str = "",
|
||||
search: str = "",
|
||||
source_kind: str = "external_upload",
|
||||
limit: int = 100,
|
||||
) -> str:
|
||||
"""רשימת הפסיקה בקורפוס הסמכותי, עם פילטרים."""
|
||||
"""רשימת הפסיקה בקורפוס, עם פילטרים.
|
||||
|
||||
source_kind: 'external_upload' (ברירת מחדל — פס"ד בתי משפט) /
|
||||
'internal_committee' (החלטות ועדות ערר ערר/בל"מ שהועלו) /
|
||||
'all_committees' (שתיהן — internal + appeals_committee).
|
||||
החלטות ערר/בל"מ שמעלים נשמרות כ-internal_committee — כדי לראותן
|
||||
ברשימה השתמש ב-source_kind='internal_committee' או 'all_committees'.
|
||||
"""
|
||||
return await plib.precedent_library_list(
|
||||
practice_area, court, precedent_level, source_type, search, limit,
|
||||
practice_area, court, precedent_level, source_type, search,
|
||||
source_kind, limit,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -97,13 +97,32 @@ def _assign_pages(chunks: list[Chunk], text: str, page_offsets: list[int]) -> No
|
||||
pos = idx + max(1, len(c.content) // 2)
|
||||
|
||||
|
||||
# A section shorter than this (stripped chars) is not a real section — it's
|
||||
# an artifact of a header keyword matched mid-text. Such a fragment is merged
|
||||
# into the preceding section rather than emitted as its own chunk. See #55:
|
||||
# unanchored keywords like "דיון"/"החלטה"/"מסקנה" appearing inside a sentence
|
||||
# used to carve tiny boundary chunks ("דיון). במסגרת ה") that polluted search.
|
||||
MIN_SECTION_CHARS = 60
|
||||
|
||||
|
||||
def _split_into_sections(text: str) -> list[tuple[str, str]]:
|
||||
"""Split text into (section_type, text) pairs based on Hebrew headers."""
|
||||
"""Split text into (section_type, text) pairs based on Hebrew headers.
|
||||
|
||||
Header keywords are matched only at the **start of a line** (after
|
||||
optional whitespace / list numbering like ``5.`` or ``ג.``). A real
|
||||
section header in these decisions sits on its own line; anchoring to
|
||||
the line start prevents common words ("דיון", "החלטה", "מסקנה") that
|
||||
appear mid-sentence from being treated as section boundaries — which
|
||||
previously produced tiny fragment chunks (#55).
|
||||
"""
|
||||
# Find all section headers and their positions
|
||||
markers: list[tuple[int, str]] = []
|
||||
|
||||
for pattern, section_type in SECTION_PATTERNS:
|
||||
for match in re.finditer(pattern, text):
|
||||
# ^ + MULTILINE: line start only. Optional leading spaces/tabs and an
|
||||
# optional ordinal prefix ("5.", "5)", "ג.") before the keyword.
|
||||
anchored = rf"^[ \t]*(?:\d+[.)]\s*|[א-ת][.)]\s*)?(?:{pattern})"
|
||||
for match in re.finditer(anchored, text, re.MULTILINE):
|
||||
markers.append((match.start(), section_type))
|
||||
|
||||
if not markers:
|
||||
@@ -120,11 +139,18 @@ def _split_into_sections(text: str) -> list[tuple[str, str]]:
|
||||
if intro_text:
|
||||
sections.append(("intro", intro_text))
|
||||
|
||||
# Each section
|
||||
# Each section. A section whose text is too short to stand alone is
|
||||
# merged into the previous section (keeping the previous type) so a
|
||||
# near-adjacent pair of headers can't produce a fragment chunk.
|
||||
for i, (pos, section_type) in enumerate(markers):
|
||||
end = markers[i + 1][0] if i + 1 < len(markers) else len(text)
|
||||
section_text = text[pos:end].strip()
|
||||
if section_text:
|
||||
if not section_text:
|
||||
continue
|
||||
if len(section_text) < MIN_SECTION_CHARS and sections:
|
||||
prev_type, prev_text = sections[-1]
|
||||
sections[-1] = (prev_type, f"{prev_text}\n{section_text}")
|
||||
else:
|
||||
sections.append((section_type, section_text))
|
||||
|
||||
return sections
|
||||
|
||||
@@ -1070,6 +1070,29 @@ ALTER TABLE case_law ADD COLUMN IF NOT EXISTS citation_formatted TEXT DEFAULT ''
|
||||
"""
|
||||
|
||||
|
||||
# ── V20: case-name / case-number lexical match ────────────────────
|
||||
# RC-A fix: the V12 tsvectors cover only chunk *content* + halacha
|
||||
# text, so a bare case-name query ("אגסי") matched decisions that
|
||||
# *cite* the case rather than the case itself. case_name and
|
||||
# case_number live on the parent case_law row, so we add a dedicated
|
||||
# meta tsvector there and OR it into the lexical search — a name/number
|
||||
# hit then surfaces all of that case's chunks + halachot. 'simple'
|
||||
# config (no stemmer) preserves Hebrew names + alphanumeric case
|
||||
# numbers like "81002-01-21" exactly as V12 does for content.
|
||||
SCHEMA_V20_SQL = """
|
||||
ALTER TABLE case_law
|
||||
ADD COLUMN IF NOT EXISTS meta_tsv tsvector
|
||||
GENERATED ALWAYS AS (
|
||||
to_tsvector('simple',
|
||||
coalesce(case_name,'') || ' ' || coalesce(case_number,'')
|
||||
)
|
||||
) STORED;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_case_law_meta_tsv
|
||||
ON case_law USING GIN(meta_tsv);
|
||||
"""
|
||||
|
||||
|
||||
async def _run_schema_migrations(pool: asyncpg.Pool) -> None:
|
||||
async with pool.acquire() as conn:
|
||||
await conn.execute(SCHEMA_SQL)
|
||||
@@ -1092,7 +1115,8 @@ async def _run_schema_migrations(pool: asyncpg.Pool) -> None:
|
||||
await conn.execute(SCHEMA_V17_SQL)
|
||||
await conn.execute(SCHEMA_V18_SQL)
|
||||
await conn.execute(SCHEMA_V19_SQL)
|
||||
logger.info("Database schema initialized (v1-v19)")
|
||||
await conn.execute(SCHEMA_V20_SQL)
|
||||
logger.info("Database schema initialized (v1-v20)")
|
||||
|
||||
|
||||
async def init_schema() -> None:
|
||||
@@ -3217,6 +3241,9 @@ async def search_precedent_library_semantic(
|
||||
ON parent.id = pc.parent_chunk_id
|
||||
WHERE {' AND '.join(chunk_filters)}
|
||||
AND pc.embedding IS NOT NULL
|
||||
-- #55: exclude tiny fragment chunks (artifacts of pre-fix
|
||||
-- mid-sentence header splits) that carry no retrievable signal.
|
||||
AND length(trim(pc.content)) >= 50
|
||||
ORDER BY pc.embedding <=> $1
|
||||
LIMIT $2
|
||||
"""
|
||||
@@ -3411,11 +3438,17 @@ async def search_precedent_library_lexical(
|
||||
h.practice_areas, h.subject_tags, h.confidence, h.rule_type,
|
||||
cl.case_number, cl.case_name, cl.court, cl.date AS decision_date,
|
||||
cl.precedent_level, cl.chair_name, cl.district,
|
||||
ts_rank_cd(h.rule_tsv, plainto_tsquery('simple', $1)) AS score
|
||||
GREATEST(
|
||||
ts_rank_cd(h.rule_tsv, plainto_tsquery('simple', $1)),
|
||||
ts_rank_cd(cl.meta_tsv, plainto_tsquery('simple', $1))
|
||||
)
|
||||
+ CASE WHEN cl.meta_tsv @@ plainto_tsquery('simple', $1)
|
||||
THEN 1.0 ELSE 0.0 END AS score
|
||||
FROM halachot h
|
||||
JOIN case_law cl ON cl.id = h.case_law_id
|
||||
WHERE {' AND '.join(halacha_filters)}
|
||||
AND h.rule_tsv @@ plainto_tsquery('simple', $1)
|
||||
AND (h.rule_tsv @@ plainto_tsquery('simple', $1)
|
||||
OR cl.meta_tsv @@ plainto_tsquery('simple', $1))
|
||||
ORDER BY score DESC
|
||||
LIMIT $2
|
||||
"""
|
||||
@@ -3439,14 +3472,22 @@ async def search_precedent_library_lexical(
|
||||
parent.page_number AS parent_page_number,
|
||||
cl.case_number, cl.case_name, cl.court, cl.date AS decision_date,
|
||||
cl.precedent_level, cl.practice_area, cl.chair_name, cl.district,
|
||||
ts_rank_cd(pc.content_tsv, plainto_tsquery('simple', $1)) AS score
|
||||
GREATEST(
|
||||
ts_rank_cd(pc.content_tsv, plainto_tsquery('simple', $1)),
|
||||
ts_rank_cd(cl.meta_tsv, plainto_tsquery('simple', $1))
|
||||
)
|
||||
+ CASE WHEN cl.meta_tsv @@ plainto_tsquery('simple', $1)
|
||||
THEN 1.0 ELSE 0.0 END AS score
|
||||
FROM precedent_chunks pc
|
||||
JOIN case_law cl ON cl.id = pc.case_law_id
|
||||
LEFT JOIN precedent_chunks parent
|
||||
ON parent.id = pc.parent_chunk_id
|
||||
WHERE {' AND '.join(chunk_filters)}
|
||||
AND pc.embedding IS NOT NULL
|
||||
AND pc.content_tsv @@ plainto_tsquery('simple', $1)
|
||||
-- #55: exclude tiny fragment chunks (see semantic query above).
|
||||
AND length(trim(pc.content)) >= 50
|
||||
AND (pc.content_tsv @@ plainto_tsquery('simple', $1)
|
||||
OR cl.meta_tsv @@ plainto_tsquery('simple', $1))
|
||||
ORDER BY score DESC
|
||||
LIMIT $2
|
||||
"""
|
||||
|
||||
@@ -533,6 +533,7 @@ async def list_precedents(
|
||||
precedent_level: str = "",
|
||||
source_type: str = "",
|
||||
search: str = "",
|
||||
source_kind: str = "external_upload",
|
||||
limit: int = 100,
|
||||
offset: int = 0,
|
||||
) -> list[dict]:
|
||||
@@ -542,6 +543,7 @@ async def list_precedents(
|
||||
precedent_level=precedent_level,
|
||||
source_type=source_type,
|
||||
search=search,
|
||||
source_kind=source_kind,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
)
|
||||
|
||||
@@ -103,6 +103,7 @@ async def precedent_library_list(
|
||||
precedent_level: str = "",
|
||||
source_type: str = "",
|
||||
search: str = "",
|
||||
source_kind: str = "external_upload",
|
||||
limit: int = 100,
|
||||
) -> str:
|
||||
"""רשימה של פסיקה בקורפוס הסמכותי, עם פילטרים."""
|
||||
@@ -112,6 +113,7 @@ async def precedent_library_list(
|
||||
precedent_level=precedent_level,
|
||||
source_type=source_type,
|
||||
search=search,
|
||||
source_kind=source_kind,
|
||||
limit=limit,
|
||||
)
|
||||
return _ok(rows)
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
| `sync_missing_agent_skills.py` | python | סקריפט "אל-כשל" להוספת `paperclipSkillSync` ל-`הגהת מסמכים` ו-`מנתח משפטי` שפיספסו את ה-sync ההיסטורי (Gap #28). תומך `--verify`/`--dry-run`/`--apply`. גיבוי אוטומטי ל-`agents-pre-skill-sync-*.sql`. דורש `PAPERCLIP_BOARD_API_KEY` (Infisical /paperclip ב-nautilus env). idempotent. | חד-פעמי (בוצע 2026-05-04). שמור לרפרנס |
|
||||
| `sync_agents_across_companies.py` | python | **סנכרון סוכנים מ-CMP (1xxx, master) ל-CMPA (8xxx, mirror)** — Gap #25. משווה adapter_config (model/timeout/instructions/skills/etc), runtime_config (heartbeat), ושדות top-level (budget/metadata/icon/title/role). מסנן אוטומטית local skills שלא קיימים ב-mirror. לוגיקת subset (mirror יכול להחזיק יותר skills כי ה-API מוסיף required runtime skills). תומך `--verify`/`--dry-run`/`--apply [--only NAME]`. גיבוי אוטומטי. דורש `PAPERCLIP_BOARD_API_KEY`. **להריץ אחרי כל שינוי הגדרות ב-CMP.** **⚠ אם `adapter_type` שונה בין CMP ל-CMPA — הסקריפט מדלג על הסוכן עם warning. בעת מעבר adapter (למשל ל-`deepseek_local`) חובה לעדכן ידנית בשתי החברות לפני sync.** | ידני אחרי כל שינוי |
|
||||
| `fix_paperclipai_skills_drift.py` | python | סקריפט חד-פעמי (בוצע 2026-05-04) שניקה drift על `paperclipai/*` skills בין CMP ל-CMPA. הסיר `paperclip-dev` מכל 14 הסוכנים, ודאג ש-`paperclip-converting-plans-to-tasks` קיים רק על CEO ו-analyst. תומך `--apply` (ברירת מחדל: dry-run). דורש `PAPERCLIP_BOARD_API_KEY`. נשמר לרפרנס למקרה שhdrift חוזר. | חד-פעמי (בוצע) |
|
||||
| `test_retrieval_by_name.py` | python | בדיקת אחזור-לפי-שם (#52/RC-A) — מאמת ש`search_precedent_library`/`search_internal_decisions` מדרגים את ההחלטה עצמה (אגסי) מעל מי שמצטט אותה, + רגרסיות לשאילתות מהותיות. הרצה: `DOTENV_PATH=/home/chaim/.env DATA_DIR=.../data mcp-server/.venv/bin/python scripts/test_retrieval_by_name.py` (exit 0 = עבר). | ידני אחרי שינוי שכבת חיפוש |
|
||||
| `auto-sync-cases.sh` | bash | סנכרון תיקי ערר ל-Gitea — רץ כל דקה | `* * * * *` (cron) |
|
||||
| `backup-db.sh` | bash | גיבוי PostgreSQL יומי ל-`data/backups/` (gzip) | לתזמן: `0 2 * * *` |
|
||||
| `restore-db.sh` | bash | שחזור DB מגיבוי (companion ל-backup-db.sh) | ידני |
|
||||
|
||||
89
scripts/test_retrieval_by_name.py
Normal file
89
scripts/test_retrieval_by_name.py
Normal file
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env python
|
||||
"""Repro + regression test for retrieval-by-name (RC-A, tasks #52).
|
||||
|
||||
Bug: searching the precedent corpus by a bare case NAME ("אגסי") fails to
|
||||
surface the decision itself, because the lexical tsvector covers only chunk
|
||||
content + halacha text — not case_name / case_number. A name query therefore
|
||||
matches decisions that *cite* the case, not the case.
|
||||
|
||||
Run with the MCP venv:
|
||||
DOTENV_PATH=/home/chaim/.env DATA_DIR=/home/chaim/legal-ai/data \
|
||||
mcp-server/.venv/bin/python scripts/test_retrieval_by_name.py
|
||||
|
||||
Exit 0 = all assertions pass. Non-zero = failure (prints what was found).
|
||||
"""
|
||||
import asyncio
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, "/home/chaim/legal-ai/mcp-server/src")
|
||||
|
||||
from legal_mcp.services import embeddings, hybrid_search # noqa: E402
|
||||
|
||||
AGASI_ID = "1a87efe5-6e13-4ed4-a9ec-3f2f7d61e4ec"
|
||||
# Vinfeld CITES Agasi (its halacha quote names אגסי) but is NOT Agasi.
|
||||
# An exact name match must rank the case itself above any case citing it.
|
||||
VINFELD_ID = "bd5d849c-c15f-43c3-96ab-d44337af9cb5"
|
||||
NAME_QUERY = "אגסי"
|
||||
SUBSTANTIVE_QUERY = 'פטור היטל השבחה לפי סעיף 19(ג)(1) שתי דירות 140 מ"ר אחת מושכרת'
|
||||
|
||||
|
||||
def _ids(rows):
|
||||
return [str(r.get("case_law_id")) for r in rows]
|
||||
|
||||
|
||||
def _rank_of(rows, cid):
|
||||
for i, r in enumerate(rows, 1):
|
||||
if str(r.get("case_law_id")) == cid:
|
||||
return i
|
||||
return None
|
||||
|
||||
|
||||
async def _search(query, source_kind, limit=10):
|
||||
query_emb = await embeddings.embed_query(query)
|
||||
return await hybrid_search.search_precedent_library_hybrid(
|
||||
query,
|
||||
query_emb,
|
||||
source_kind=source_kind,
|
||||
limit=limit,
|
||||
include_halachot=True,
|
||||
)
|
||||
|
||||
|
||||
async def main():
|
||||
results = {"pass": [], "fail": []}
|
||||
|
||||
# 1) THE BUG: bare-name query must rank the case ITSELF (Agasi) above any
|
||||
# case that merely CITES it (Vinfeld), and within the top 3.
|
||||
rows = await _search(NAME_QUERY, "internal_committee", limit=10)
|
||||
a_rank = _rank_of(rows, AGASI_ID)
|
||||
v_rank = _rank_of(rows, VINFELD_ID)
|
||||
ok = bool(a_rank) and a_rank <= 3 and (v_rank is None or a_rank < v_rank)
|
||||
msg = (f"[name/internal] query='{NAME_QUERY}' -> Agasi rank={a_rank}, "
|
||||
f"Vinfeld(citer) rank={v_rank} (top ids: {_ids(rows)[:5]})")
|
||||
(results["pass"] if ok else results["fail"]).append(msg)
|
||||
|
||||
# 2) REGRESSION: substantive query must still find Agasi with a real score.
|
||||
rows = await _search(SUBSTANTIVE_QUERY, "internal_committee", limit=10)
|
||||
rank = _rank_of(rows, AGASI_ID)
|
||||
top_score = float(rows[0]["score"]) if rows else 0.0
|
||||
msg = f"[substantive/internal] Agasi rank={rank}, top_score={top_score:.3f}"
|
||||
(results["pass"] if rank and rank <= 8 else results["fail"]).append(msg)
|
||||
|
||||
# 3) REGRESSION: substantive query in the full precedent library still works
|
||||
# (Vinfeld/נווה שלום etc. should surface; just assert non-empty + has betterment content).
|
||||
rows = await _search(SUBSTANTIVE_QUERY, "external_upload", limit=10)
|
||||
msg = f"[substantive/external] returned {len(rows)} rows (top ids: {_ids(rows)[:3]})"
|
||||
(results["pass"] if len(rows) >= 3 else results["fail"]).append(msg)
|
||||
|
||||
print("\n=== PASS ===")
|
||||
for m in results["pass"]:
|
||||
print(" ✓", m)
|
||||
print("=== FAIL ===")
|
||||
for m in results["fail"]:
|
||||
print(" ✗", m)
|
||||
|
||||
return 1 if results["fail"] else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(asyncio.run(main()))
|
||||
Reference in New Issue
Block a user