feat(precedents): minimum-effort upload — file+citation, rest auto-extracted
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m35s
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m35s
The missing-precedents drawer + general precedent upload both required the user to type chair_name, district, practice_area, court, date etc. upfront — even though those fields can be (and already are, post-upload) extracted from the document text by the LLM. The metadata-extraction wakeup also only fired for the /precedent-library/upload path, leaving missing-precedents committee uploads stuck with whatever stub the user typed. Changes: - Extractor learns chair_name + district, overwrites the new PLACEHOLDER_PENDING_EXTRACTION sentinel for internal_committee rows (the DB CHECK forces non-empty; we stamp the placeholder at insert). - missing_precedent_upload no longer 400s on missing chair/district; it infers district from the citation when possible, falls back to the placeholder, and always fires pc_wake_for_precedent_extraction so the LLM can fill in the rest. - Both upload sheets default to file (+ citation) only; every other field is tucked into a closed <details> labeled "אופציונלי — דריסה ידנית של שדות שיחולצו אוטומטית". Required validators on chair/ district/practice_area dropped — the LLM fills them. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,9 @@
|
||||
Runs after chunking. Reads the precedent's full_text and asks Claude to
|
||||
fill in the metadata fields that an upload form usually leaves empty:
|
||||
short case_name, summary, headnote, key_quote, subject_tags,
|
||||
appeal_subtype, decision_date, precedent_level, court.
|
||||
appeal_subtype, decision_date, precedent_level, court — plus
|
||||
chair_name + district for internal_committee rows (which the upload
|
||||
path stamps with PLACEHOLDER_PENDING_EXTRACTION when missing).
|
||||
|
||||
Caller policy: only empty user-supplied fields are filled. Anything the
|
||||
chair already typed in the upload form is preserved. This is enforced
|
||||
@@ -22,6 +24,12 @@ from legal_mcp.services import claude_session, db
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Sentinel inserted by the upload endpoint when a committee row is created
|
||||
# without chair_name/district (the DB CHECK forces non-empty). Treated as
|
||||
# empty by ``apply_to_record`` so LLM-extracted values overwrite it.
|
||||
PLACEHOLDER_PENDING_EXTRACTION = "(טרם חולץ)"
|
||||
|
||||
|
||||
# The prompt is short — we only need the first 12K chars of the ruling
|
||||
# (header + opening of discussion is enough for naming + summary). For
|
||||
# subject tags we sample the discussion section too.
|
||||
@@ -52,7 +60,9 @@ METADATA_EXTRACTION_PROMPT = """אתה מסייע משפטי בכיר. קרא א
|
||||
"source_type": "אחד מ-2: 'court_ruling' (פסק דין של בית משפט — עליון/מנהלי) / 'appeals_committee' (החלטה של ועדת ערר). אם לא ברור — מחרוזת ריקה.",
|
||||
"proceeding_type": "אחד מ-2 (רק להחלטות ועדת ערר): 'ערר' (הליך ערר עיקרי על החלטת ועדה מקומית) / 'בל\\\"מ' (בקשה להארכת מועד להגשת ערר). זהה דרך כותרת המסמך: 'ערר (ועדות ערר ...) NNNN/YY' → 'ערר'; 'בל\\\"מ NNNN/YY' או נושא 'בקשה להארכת מועד להגשת ערר' → 'בל\\\"מ'. בפסיקת בית משפט (לא ועדת ערר) — מחרוזת ריקה.",
|
||||
"court": "שם הערכאה כפי שהוא מופיע בכותרת (למשל 'בית המשפט העליון', 'בית המשפט המחוזי בירושלים בשבתו כבית משפט לעניינים מנהליים', 'ועדת הערר לתכנון ובניה פיצויים והיטלי השבחה — מחוז ירושלים'). מחרוזת ריקה אם לא ניתן לזהות.",
|
||||
"case_number_clean": "מספר הערר/תיק כפי שמופיע בכותרת — רק הספרות והאלכסון, למשל '1062/24' או '8031/21'. ללא המילה 'ערר', ללא שם הצדדים, ללא סוגריים. אם יש כמה עררים מאוחדים — הרשום הראשון. מחרוזת ריקה אם לא ניתן לזהות."
|
||||
"case_number_clean": "מספר הערר/תיק כפי שמופיע בכותרת — רק הספרות והאלכסון, למשל '1062/24' או '8031/21'. ללא המילה 'ערר', ללא שם הצדדים, ללא סוגריים. אם יש כמה עררים מאוחדים — הרשום הראשון. מחרוזת ריקה אם לא ניתן לזהות.",
|
||||
"chair_name": "שם יו\\\"ר ההרכב — רלוונטי **רק להחלטות ועדת ערר**, לא לפסקי בית משפט. חפש בכותרת/חתימה: 'עו\\\"ד דפנה תמיר, יו\\\"ר ועדת הערר', 'בפני: עו\\\"ד פלוני אלמוני (יו\\\"ר)'. השאר שם פרטי+משפחה בלי תוארים ('עו\\\"ד', 'אדריכל'). אם זה פסק דין של בית משפט — מחרוזת ריקה.",
|
||||
"district": "מחוז ועדת הערר — רלוונטי **רק להחלטות ועדת ערר**. ערכים מותרים: 'ירושלים', 'תל אביב', 'מרכז', 'חיפה', 'צפון', 'דרום', 'ארצית'. זהה מהכותרת ('ועדת הערר לתכנון ובניה — מחוז ירושלים' → 'ירושלים'; 'ועדות ערר - תכנון ובנייה תל אביב-יפו' → 'תל אביב'). אם זה פסק דין של בית משפט — מחרוזת ריקה."
|
||||
}
|
||||
|
||||
## כללי איכות
|
||||
@@ -67,6 +77,7 @@ METADATA_EXTRACTION_PROMPT = """אתה מסייע משפטי בכיר. קרא א
|
||||
9. **source_type** — שני ערכים בלבד: "court_ruling" כשהמסמך הוא פסק דין/החלטה של בית משפט (עליון/בג"ץ/מנהלי/מחוזי); "appeals_committee" כשהמסמך הוא החלטה של ועדת ערר (ארצית או מחוזית). זה משלים את `precedent_level` — שני השדות צריכים להיות תואמים.
|
||||
10. **court** — מהכותרת הראשית של הפסק. ניסוח מלא (לא קיצור). מחרוזת ריקה אם לא ניתן לזהות.
|
||||
11. **proceeding_type** — חובה לזהות עבור החלטות ועדת ערר; ריק עבור פסיקת בית משפט. הסימן הברור: בכותרת הראשונה של המסמך כתוב "ערר (ועדות ערר ...) NNNN/YY" → 'ערר'; "בל\"מ NNNN/YY" או הנושא "בקשה להארכת מועד להגשת ערר" → 'בל\"מ'. שני הסוגים יכולים לחלוק אותו מספר תיק — לכן חשוב להבחין מפורשות.
|
||||
12. **chair_name / district** — חובה למלא רק עבור החלטות ועדת ערר (source_type='appeals_committee'). chair_name נמצא בכותרת ("בפני: עו\"ד פלוני אלמוני, יו\"ר") או בחתימה. district = מחוז הוועדה, מתוך רשימה סגורה. עבור פסקי בית משפט — שני השדות ריקים.
|
||||
"""
|
||||
|
||||
|
||||
@@ -170,6 +181,14 @@ async def extract_metadata(case_law_id: UUID | str) -> dict:
|
||||
out["court"] = result["court"].strip()
|
||||
if isinstance(result.get("case_number_clean"), str):
|
||||
out["case_number_clean"] = result["case_number_clean"].strip()
|
||||
if isinstance(result.get("chair_name"), str):
|
||||
out["chair_name"] = result["chair_name"].strip()
|
||||
if isinstance(result.get("district"), str):
|
||||
d = result["district"].strip()
|
||||
# Closed enum for districts — anything else is dropped to avoid
|
||||
# silently storing free-text in what callers treat as a filter facet.
|
||||
if d in {"ירושלים", "תל אביב", "מרכז", "חיפה", "צפון", "דרום", "ארצית"}:
|
||||
out["district"] = d
|
||||
return out
|
||||
|
||||
|
||||
@@ -285,6 +304,22 @@ async def apply_to_record(
|
||||
if cn:
|
||||
fields_to_update["case_number"] = cn
|
||||
|
||||
# chair_name / district — only for internal_committee rows. The DB CHECK
|
||||
# forces these to be non-empty, so the upload endpoint stamps the row
|
||||
# with "(טרם חולץ)" as a placeholder. Treat that placeholder as empty
|
||||
# so the LLM-extracted value can overwrite it.
|
||||
if record.get("source_kind") == "internal_committee":
|
||||
cur_chair = (record.get("chair_name") or "").strip()
|
||||
if cur_chair in ("", PLACEHOLDER_PENDING_EXTRACTION):
|
||||
s = (suggested.get("chair_name") or "").strip()
|
||||
if s:
|
||||
fields_to_update["chair_name"] = s
|
||||
cur_district = (record.get("district") or "").strip()
|
||||
if cur_district in ("", PLACEHOLDER_PENDING_EXTRACTION):
|
||||
s = (suggested.get("district") or "").strip()
|
||||
if s:
|
||||
fields_to_update["district"] = s
|
||||
|
||||
if not fields_to_update:
|
||||
return {"updated": False, "fields": []}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user