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": []}
|
||||
|
||||
|
||||
@@ -121,17 +121,13 @@ export function MissingPrecedentDetailDrawer({ id, onOpenChange }: Props) {
|
||||
toast.error("בחר קובץ");
|
||||
return;
|
||||
}
|
||||
if (isCommittee && (!chairName.trim() || !district.trim())) {
|
||||
toast.error("החלטת ועדת ערר דורשת שם יו״ר ומחוז");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const result = await upload.mutateAsync({
|
||||
await upload.mutateAsync({
|
||||
id: mp.id,
|
||||
file,
|
||||
case_number: isCommittee ? committeeCaseNumber || undefined : undefined,
|
||||
chair_name: isCommittee ? chairName : undefined,
|
||||
district: isCommittee ? district : undefined,
|
||||
chair_name: isCommittee ? chairName || undefined : undefined,
|
||||
district: isCommittee ? district || undefined : undefined,
|
||||
case_name: caseName || undefined,
|
||||
court: court || undefined,
|
||||
decision_date: decisionDate || undefined,
|
||||
@@ -142,7 +138,7 @@ export function MissingPrecedentDetailDrawer({ id, onOpenChange }: Props) {
|
||||
summary: summary || undefined,
|
||||
});
|
||||
toast.success(
|
||||
`הפסיקה נכנסה לקורפוס (${result.route === "internal_committee" ? "ועדת ערר" : "פסק דין"}) והרשומה נסגרה.`,
|
||||
"הקובץ הועלה. חילוץ המטא־דאטה (שם, ערכאה, תאריך, יו״ר, מחוז…) מתבצע ברקע ויסתיים בתוך כדקה.",
|
||||
);
|
||||
onOpenChange(false);
|
||||
} catch (e: unknown) {
|
||||
@@ -341,11 +337,14 @@ export function MissingPrecedentDetailDrawer({ id, onOpenChange }: Props) {
|
||||
<h3 className="text-sm font-semibold text-navy">
|
||||
העלאת הפסיקה לקורפוס
|
||||
</h3>
|
||||
<div className="text-[0.78rem] text-ink-muted">
|
||||
<div className="text-[0.78rem] text-ink-muted leading-relaxed">
|
||||
ניתוב אוטומטי לפי הציטוט:
|
||||
<strong className="text-navy">
|
||||
{isCommittee ? "החלטת ועדת ערר (internal)" : "פסק דין (library)"}
|
||||
</strong>
|
||||
<br />
|
||||
שדות נוספים (שם, ערכאה, תאריך, יו״ר, מחוז, תת־סוג) יחולצו אוטומטית
|
||||
מהקובץ ברקע.
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleUpload} className="space-y-3">
|
||||
@@ -360,134 +359,137 @@ export function MissingPrecedentDetailDrawer({ id, onOpenChange }: Props) {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<Label htmlFor="court">ערכאה</Label>
|
||||
<Input
|
||||
id="court"
|
||||
value={court}
|
||||
onChange={(e) => setCourt(e.target.value)}
|
||||
placeholder="בית המשפט העליון"
|
||||
dir="rtl"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="decision_date">תאריך</Label>
|
||||
<Input
|
||||
id="decision_date"
|
||||
type="date"
|
||||
value={decisionDate}
|
||||
onChange={(e) => setDecisionDate(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<details className="group rounded-md border border-rule bg-rule-soft/30">
|
||||
<summary className="cursor-pointer select-none px-3 py-2 text-[0.78rem] text-ink-muted hover:text-navy">
|
||||
אופציונלי — דריסה ידנית של שדות שיחולצו אוטומטית
|
||||
</summary>
|
||||
<div className="space-y-3 border-t border-rule px-3 py-3">
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<Label htmlFor="practice_area">תחום</Label>
|
||||
<Select value={practiceArea} onValueChange={setPracticeArea}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="ללא" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{PRACTICE_AREAS.map((a) => (
|
||||
<SelectItem key={a.value} value={a.value}>
|
||||
{a.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="appeal_subtype">תת־סוג</Label>
|
||||
<Input
|
||||
id="appeal_subtype"
|
||||
value={appealSubtype}
|
||||
onChange={(e) => setAppealSubtype(e.target.value)}
|
||||
placeholder="זכות עמידה"
|
||||
dir="rtl"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isCommittee ? (
|
||||
<>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<Label htmlFor="chair_name">
|
||||
יו״ר <span className="text-danger">*</span>
|
||||
</Label>
|
||||
<Label htmlFor="court">ערכאה</Label>
|
||||
<Input
|
||||
id="chair_name"
|
||||
value={chairName}
|
||||
onChange={(e) => setChairName(e.target.value)}
|
||||
placeholder="דפנה תמיר"
|
||||
id="court"
|
||||
value={court}
|
||||
onChange={(e) => setCourt(e.target.value)}
|
||||
placeholder="בית המשפט העליון"
|
||||
dir="rtl"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="district">
|
||||
מחוז <span className="text-danger">*</span>
|
||||
</Label>
|
||||
<Select value={district} onValueChange={setDistrict}>
|
||||
<Label htmlFor="decision_date">תאריך</Label>
|
||||
<Input
|
||||
id="decision_date"
|
||||
type="date"
|
||||
value={decisionDate}
|
||||
onChange={(e) => setDecisionDate(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<Label htmlFor="practice_area">תחום</Label>
|
||||
<Select value={practiceArea} onValueChange={setPracticeArea}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="בחר" />
|
||||
<SelectValue placeholder="ללא" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{DISTRICTS.map((d) => (
|
||||
<SelectItem key={d.value} value={d.value}>
|
||||
{d.label}
|
||||
{PRACTICE_AREAS.map((a) => (
|
||||
<SelectItem key={a.value} value={a.value}>
|
||||
{a.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="appeal_subtype">תת־סוג</Label>
|
||||
<Input
|
||||
id="appeal_subtype"
|
||||
value={appealSubtype}
|
||||
onChange={(e) => setAppealSubtype(e.target.value)}
|
||||
placeholder="זכות עמידה"
|
||||
dir="rtl"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isCommittee ? (
|
||||
<>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<Label htmlFor="chair_name">יו״ר</Label>
|
||||
<Input
|
||||
id="chair_name"
|
||||
value={chairName}
|
||||
onChange={(e) => setChairName(e.target.value)}
|
||||
placeholder="דפנה תמיר"
|
||||
dir="rtl"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="district">מחוז</Label>
|
||||
<Select value={district} onValueChange={setDistrict}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="בחר" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{DISTRICTS.map((d) => (
|
||||
<SelectItem key={d.value} value={d.value}>
|
||||
{d.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="committee_case_number">
|
||||
מספר ערר (לציטוט הקטן)
|
||||
</Label>
|
||||
<Input
|
||||
id="committee_case_number"
|
||||
value={committeeCaseNumber}
|
||||
onChange={(e) => setCommitteeCaseNumber(e.target.value)}
|
||||
placeholder="ערר 1112/22 ..."
|
||||
dir="rtl"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div>
|
||||
<Label htmlFor="precedent_level">רמת תקדים</Label>
|
||||
<Select
|
||||
value={precedentLevel}
|
||||
onValueChange={setPrecedentLevel}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="ללא" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{PRECEDENT_LEVELS.map((l) => (
|
||||
<SelectItem key={l.value} value={l.value}>
|
||||
{l.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<Label htmlFor="committee_case_number">
|
||||
מספר ערר (לציטוט הקטן)
|
||||
</Label>
|
||||
<Input
|
||||
id="committee_case_number"
|
||||
value={committeeCaseNumber}
|
||||
onChange={(e) => setCommitteeCaseNumber(e.target.value)}
|
||||
placeholder="ערר 1112/22 ..."
|
||||
<Label htmlFor="summary">תקציר</Label>
|
||||
<Textarea
|
||||
id="summary"
|
||||
value={summary}
|
||||
onChange={(e) => setSummary(e.target.value)}
|
||||
rows={2}
|
||||
dir="rtl"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div>
|
||||
<Label htmlFor="precedent_level">רמת תקדים</Label>
|
||||
<Select
|
||||
value={precedentLevel}
|
||||
onValueChange={setPrecedentLevel}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="ללא" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{PRECEDENT_LEVELS.map((l) => (
|
||||
<SelectItem key={l.value} value={l.value}>
|
||||
{l.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<Label htmlFor="summary">תקציר</Label>
|
||||
<Textarea
|
||||
id="summary"
|
||||
value={summary}
|
||||
onChange={(e) => setSummary(e.target.value)}
|
||||
rows={2}
|
||||
dir="rtl"
|
||||
/>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
|
||||
@@ -93,10 +93,6 @@ export function PrecedentUploadSheet({ open, onOpenChange }: Props) {
|
||||
toast.error("מראה המקום (citation) חובה");
|
||||
return;
|
||||
}
|
||||
if (!practiceArea) {
|
||||
toast.error("בחר תחום משפט");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const tags = subjectTags
|
||||
.split(",")
|
||||
@@ -132,8 +128,9 @@ export function PrecedentUploadSheet({ open, onOpenChange }: Props) {
|
||||
<SheetHeader>
|
||||
<SheetTitle className="text-navy">העלאת פסיקה לקורפוס הסמכותי</SheetTitle>
|
||||
<SheetDescription className="text-ink-muted">
|
||||
הקובץ יעבור חילוץ טקסט, יצירת embeddings, וחילוץ הלכות אוטומטי.
|
||||
ההלכות יחכו לאישורך לפני שהן זמינות לסוכני הכתיבה.
|
||||
הקובץ יעבור חילוץ טקסט, embeddings, וחילוץ אוטומטי של מטא־דאטה
|
||||
(שם, ערכאה, תאריך, תחום, תת־סוג, תגיות) והלכות. ההלכות ימתינו
|
||||
לאישורך לפני שיהיו זמינות לסוכני הכתיבה.
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
|
||||
@@ -159,104 +156,109 @@ export function PrecedentUploadSheet({ open, onOpenChange }: Props) {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Two-col grid */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="case-name">שם קצר</Label>
|
||||
<Input id="case-name" value={caseName}
|
||||
onChange={(e) => setCaseName(e.target.value)}
|
||||
placeholder="ב. קרן-נכסים" disabled={isProcessing} />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="court">ערכאה</Label>
|
||||
<Input id="court" value={court}
|
||||
onChange={(e) => setCourt(e.target.value)}
|
||||
placeholder='בית משפט עליון / בג"ץ / מנהלי / ועדת ערר'
|
||||
disabled={isProcessing} />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="date">תאריך החלטה</Label>
|
||||
<Input id="date" type="date" value={decisionDate}
|
||||
onChange={(e) => setDecisionDate(e.target.value)}
|
||||
disabled={isProcessing} />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="appeal-subtype">תת-סוג (חופשי)</Label>
|
||||
<Input id="appeal-subtype" value={appealSubtype}
|
||||
onChange={(e) => setAppealSubtype(e.target.value)}
|
||||
placeholder="שימוש חורג / סופיות ההחלטה" disabled={isProcessing} />
|
||||
</div>
|
||||
</div>
|
||||
<details className="group rounded-md border border-rule bg-rule-soft/30">
|
||||
<summary className="cursor-pointer select-none px-3 py-2 text-[0.78rem] text-ink-muted hover:text-navy">
|
||||
אופציונלי — דריסה ידנית של שדות שיחולצו אוטומטית מהמסמך
|
||||
</summary>
|
||||
<div className="space-y-3 border-t border-rule px-3 py-3">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="case-name">שם קצר</Label>
|
||||
<Input id="case-name" value={caseName}
|
||||
onChange={(e) => setCaseName(e.target.value)}
|
||||
placeholder="ב. קרן-נכסים" disabled={isProcessing} />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="court">ערכאה</Label>
|
||||
<Input id="court" value={court}
|
||||
onChange={(e) => setCourt(e.target.value)}
|
||||
placeholder='בית משפט עליון / בג"ץ / מנהלי / ועדת ערר'
|
||||
disabled={isProcessing} />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="date">תאריך החלטה</Label>
|
||||
<Input id="date" type="date" value={decisionDate}
|
||||
onChange={(e) => setDecisionDate(e.target.value)}
|
||||
disabled={isProcessing} />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="appeal-subtype">תת-סוג (חופשי)</Label>
|
||||
<Input id="appeal-subtype" value={appealSubtype}
|
||||
onChange={(e) => setAppealSubtype(e.target.value)}
|
||||
placeholder="שימוש חורג / סופיות ההחלטה" disabled={isProcessing} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Practice area (required radio) */}
|
||||
<div className="space-y-1">
|
||||
<Label>תחום משפט (חובה)</Label>
|
||||
<div className="flex gap-4 flex-wrap">
|
||||
{PRACTICE_AREAS.map((a) => (
|
||||
<label key={a.value} className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="radio" name="practice_area" value={a.value}
|
||||
checked={practiceArea === a.value}
|
||||
onChange={() => setPracticeArea(a.value as PracticeArea)}
|
||||
disabled={isProcessing}
|
||||
/>
|
||||
<span className="text-sm text-ink">{a.label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="source-type">סוג מקור</Label>
|
||||
<Select value={sourceType || "_none"}
|
||||
onValueChange={(v) => setSourceType(v === "_none" ? "" : v as SourceType)}
|
||||
disabled={isProcessing}>
|
||||
<SelectTrigger><SelectValue placeholder="—" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="_none">—</SelectItem>
|
||||
{SOURCE_TYPES.map((s) => (
|
||||
<SelectItem key={s.value} value={s.value}>{s.label}</SelectItem>
|
||||
<div className="space-y-1">
|
||||
<Label>תחום משפט</Label>
|
||||
<div className="flex gap-4 flex-wrap">
|
||||
{PRACTICE_AREAS.map((a) => (
|
||||
<label key={a.value} className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="radio" name="practice_area" value={a.value}
|
||||
checked={practiceArea === a.value}
|
||||
onChange={() => setPracticeArea(a.value as PracticeArea)}
|
||||
disabled={isProcessing}
|
||||
/>
|
||||
<span className="text-sm text-ink">{a.label}</span>
|
||||
</label>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="source-type">סוג מקור</Label>
|
||||
<Select value={sourceType || "_none"}
|
||||
onValueChange={(v) => setSourceType(v === "_none" ? "" : v as SourceType)}
|
||||
disabled={isProcessing}>
|
||||
<SelectTrigger><SelectValue placeholder="—" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="_none">—</SelectItem>
|
||||
{SOURCE_TYPES.map((s) => (
|
||||
<SelectItem key={s.value} value={s.value}>{s.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="precedent-level">רמת תקדים</Label>
|
||||
<Select value={precedentLevel || "_none"}
|
||||
onValueChange={(v) => setPrecedentLevel(v === "_none" ? "" : v)}
|
||||
disabled={isProcessing}>
|
||||
<SelectTrigger><SelectValue placeholder="—" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="_none">—</SelectItem>
|
||||
{PRECEDENT_LEVELS.map((l) => (
|
||||
<SelectItem key={l.value} value={l.value}>{l.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="tags">תגיות נושא (מופרדות בפסיקים)</Label>
|
||||
<Input id="tags" value={subjectTags}
|
||||
onChange={(e) => setSubjectTags(e.target.value)}
|
||||
placeholder="חניה, קווי בניין, שיקול דעת" disabled={isProcessing} />
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="headnote">תקציר / headnote</Label>
|
||||
<Textarea id="headnote" value={headnote} rows={2}
|
||||
onChange={(e) => setHeadnote(e.target.value)}
|
||||
placeholder="תקציר חופשי שיוצג ברשימה" disabled={isProcessing} dir="rtl" />
|
||||
</div>
|
||||
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" checked={isBinding}
|
||||
onChange={(e) => setIsBinding(e.target.checked)}
|
||||
disabled={isProcessing} />
|
||||
<span className="text-sm">הלכה מחייבת</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="precedent-level">רמת תקדים</Label>
|
||||
<Select value={precedentLevel || "_none"}
|
||||
onValueChange={(v) => setPrecedentLevel(v === "_none" ? "" : v)}
|
||||
disabled={isProcessing}>
|
||||
<SelectTrigger><SelectValue placeholder="—" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="_none">—</SelectItem>
|
||||
{PRECEDENT_LEVELS.map((l) => (
|
||||
<SelectItem key={l.value} value={l.value}>{l.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="tags">תגיות נושא (מופרדות בפסיקים)</Label>
|
||||
<Input id="tags" value={subjectTags}
|
||||
onChange={(e) => setSubjectTags(e.target.value)}
|
||||
placeholder="חניה, קווי בניין, שיקול דעת" disabled={isProcessing} />
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="headnote">תקציר / headnote (אופציונלי)</Label>
|
||||
<Textarea id="headnote" value={headnote} rows={2}
|
||||
onChange={(e) => setHeadnote(e.target.value)}
|
||||
placeholder="תקציר חופשי שיוצג ברשימה" disabled={isProcessing} dir="rtl" />
|
||||
</div>
|
||||
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" checked={isBinding}
|
||||
onChange={(e) => setIsBinding(e.target.checked)}
|
||||
disabled={isProcessing} />
|
||||
<span className="text-sm">הלכה מחייבת</span>
|
||||
</label>
|
||||
</details>
|
||||
|
||||
{isProcessing && (
|
||||
<div className="rounded-lg border border-rule bg-rule-soft/40 p-4 space-y-2">
|
||||
|
||||
41
web/app.py
41
web/app.py
@@ -4416,6 +4416,9 @@ async def _process_training_document(task_id: str, source: Path, req: ClassifyRe
|
||||
# corpus) and /api/cases/{n}/precedents (chair-attached quotes).
|
||||
|
||||
from legal_mcp.services import precedent_library as plib_service # noqa: E402
|
||||
from legal_mcp.services.precedent_metadata_extractor import ( # noqa: E402
|
||||
PLACEHOLDER_PENDING_EXTRACTION,
|
||||
)
|
||||
|
||||
|
||||
_PRACTICE_AREAS = {"", "rishuy_uvniya", "betterment_levy", "compensation_197"}
|
||||
@@ -5237,11 +5240,20 @@ async def missing_precedent_upload(
|
||||
|
||||
try:
|
||||
if is_committee:
|
||||
if not chair_name.strip() or not district.strip():
|
||||
raise HTTPException(
|
||||
400,
|
||||
"החלטת ועדת ערר דורשת chair_name + district",
|
||||
)
|
||||
# The DB CHECK forces chair_name + district to be non-empty for
|
||||
# internal_committee rows. The UX goal is "upload file + citation
|
||||
# only" — so if the user didn't fill those, infer district from
|
||||
# the citation text (often contains the committee name, e.g.
|
||||
# "ועדות ערר - תכנון ובנייה תל אביב-יפו") and fall back to a
|
||||
# placeholder. The metadata extractor wakeup fired below will
|
||||
# overwrite both placeholders once the LLM reads the file.
|
||||
resolved_chair = chair_name.strip() or PLACEHOLDER_PENDING_EXTRACTION
|
||||
resolved_district = (
|
||||
district.strip()
|
||||
or int_decisions_service._district_from_court(court)
|
||||
or int_decisions_service._district_from_court(citation)
|
||||
or PLACEHOLDER_PENDING_EXTRACTION
|
||||
)
|
||||
# case_number for the committee decision (not the cited-in case)
|
||||
committee_case_number = case_number.strip() or citation
|
||||
result = await int_decisions_service.ingest_internal_decision(
|
||||
@@ -5249,8 +5261,8 @@ async def missing_precedent_upload(
|
||||
case_name=(case_name.strip() or mp.get("case_name") or "").strip(),
|
||||
court=court.strip(),
|
||||
decision_date=decision_date or None,
|
||||
chair_name=chair_name.strip(),
|
||||
district=district.strip(),
|
||||
chair_name=resolved_chair,
|
||||
district=resolved_district,
|
||||
practice_area=practice_area,
|
||||
appeal_subtype=appeal_subtype.strip(),
|
||||
subject_tags=tags,
|
||||
@@ -5298,6 +5310,21 @@ async def missing_precedent_upload(
|
||||
except Exception as e:
|
||||
logger.exception("missing-precedent close failed")
|
||||
raise HTTPException(500, f"קישור הרשומה נכשל: {e}")
|
||||
|
||||
# Fire metadata-extraction wakeup so the placeholder fields above
|
||||
# (and any other empty user-supplied fields) get filled in from the
|
||||
# file's text. Best-effort: mirrors the precedent_library_upload
|
||||
# contract — failures are logged, not surfaced.
|
||||
try:
|
||||
await pc_wake_for_precedent_extraction(
|
||||
case_law_id=case_law_id,
|
||||
citation=citation,
|
||||
practice_area=practice_area,
|
||||
)
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"missing-precedent: precedent-extraction wakeup failed (non-fatal)"
|
||||
)
|
||||
finally:
|
||||
staged.unlink(missing_ok=True)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user