fix(learning): extract precedent metadata inline on final-decision enroll
When a chair's signed final decision is uploaded it is enrolled into the precedent library, but _enroll_final_in_library only set deterministic fields (citation, proceeding_type, date) and copied subject_tags from the case's subject_categories — which is usually empty. The Gemini metadata pass (subject_tags / summary / headnote / key_quote) was never triggered, so the row sat at metadata_extraction_status='pending' with no subject tags until a drain happened to pick it up. In practice a freshly-enrolled final showed an empty "תגיות נושא" in the precedent edit UI. Fix: after enrollment + citation, call the existing reextract_metadata path inline (G2 — one path, full status lifecycle). It runs on Gemini Flash over REST (GOOGLE_GEMINI_API_KEY, already in Coolify), so it is container-safe — unlike the halacha path (claude CLI, host-only). apply_to_record fills only empty fields, so the deterministic seeds and any chair-curated subject_categories are preserved. Result surfaced in the upload response (out["metadata"]); failures logged, upload still succeeds. Also corrects two now-stale comments: the "Gemini returns no_metadata for internal decisions" note in the enroll loop, and the "MCP-tool-only path" docstring on reextract_metadata (true for halacha, not for Gemini REST). Invariants: G1 (fill at source on enroll, not a read-time workaround), G2 (reuse reextract_metadata — no parallel extraction path). LLM call is Gemini REST, not claude_session, so the container LLM-call constraint holds. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -383,7 +383,10 @@ async def reextract_metadata(
|
|||||||
appeal_subtype, and case_name when it equals the citation). User
|
appeal_subtype, and case_name when it equals the citation). User
|
||||||
values are preserved.
|
values are preserved.
|
||||||
|
|
||||||
**MCP-tool-only path** — same constraint as :func:`reextract_halachot`.
|
**Container-safe** — unlike :func:`reextract_halachot` (claude CLI, host-only),
|
||||||
|
metadata extraction runs on Gemini Flash over REST (GOOGLE_GEMINI_API_KEY), so
|
||||||
|
this path is callable from the FastAPI container too. The final-decision
|
||||||
|
enrollment loop (``_enroll_final_in_library``) calls it inline on upload.
|
||||||
"""
|
"""
|
||||||
from legal_mcp.services import precedent_metadata_extractor
|
from legal_mcp.services import precedent_metadata_extractor
|
||||||
|
|
||||||
|
|||||||
28
web/app.py
28
web/app.py
@@ -3643,9 +3643,11 @@ async def _enroll_final_in_library(
|
|||||||
out["error"] = "no final text extracted"
|
out["error"] = "no final text extracted"
|
||||||
return out
|
return out
|
||||||
|
|
||||||
# Deterministic metadata from the case record — the Gemini metadata extractor is
|
# Deterministic seeds from the case record first — proceeding_type / date /
|
||||||
# tuned for EXTERNAL rulings and returns no_metadata for internal decisions, so we
|
# citation are identity, not inference, so we set them ourselves (no LLM) and the
|
||||||
# populate proceeding_type / date / tags / summary / citation ourselves (no LLM).
|
# later Gemini pass (reextract_metadata, below) is fills-empty-only and won't
|
||||||
|
# touch them. subject_tags/summary seeded here from the case (chair-curated
|
||||||
|
# subject_categories win when present); when empty, Gemini fills them inline.
|
||||||
district = "ירושלים"
|
district = "ירושלים"
|
||||||
proceeding_type = (case.get("proceeding_type") or "ערר").strip()
|
proceeding_type = (case.get("proceeding_type") or "ערר").strip()
|
||||||
decision_date = case.get("decision_date") or case.get("hearing_date")
|
decision_date = case.get("decision_date") or case.get("hearing_date")
|
||||||
@@ -3685,6 +3687,26 @@ async def _enroll_final_in_library(
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning("citation build failed for %s: %s", case_number, e)
|
logger.warning("citation build failed for %s: %s", case_number, e)
|
||||||
|
|
||||||
|
# Fill the LLM-derived metadata (subject_tags, summary, headnote, key_quote)
|
||||||
|
# inline via Gemini (REST — container-safe; GOOGLE_GEMINI_API_KEY in Coolify),
|
||||||
|
# reusing the same reextract_metadata path the UI/drain use (G2 — one path, full
|
||||||
|
# status lifecycle). apply_to_record fills ONLY empty fields, so the deterministic
|
||||||
|
# seeds above (proceeding_type / date / citation, plus any chair-curated
|
||||||
|
# subject_categories) are preserved. Without this the row sat at
|
||||||
|
# metadata_extraction_status='pending' with empty tags until a drain happened to
|
||||||
|
# pick it up, so a freshly-enrolled final showed no subject tags in the edit UI.
|
||||||
|
# Best-effort — surfaced in the return value, never silently swallowed.
|
||||||
|
try:
|
||||||
|
from legal_mcp.services import precedent_library as plib_service
|
||||||
|
meta = await plib_service.reextract_metadata(UUID(case_law_id))
|
||||||
|
out["metadata"] = {
|
||||||
|
"status": meta.get("status"),
|
||||||
|
"fields": meta.get("fields") or [],
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("inline metadata extraction failed for %s: %s", case_number, e)
|
||||||
|
out["metadata_error"] = str(e)
|
||||||
|
|
||||||
# The precedents this decision cites → link to the library; flag the ones not found.
|
# The precedents this decision cites → link to the library; flag the ones not found.
|
||||||
try:
|
try:
|
||||||
await cit_tools.extract_internal_citations(case_law_id=case_law_id, limit=0)
|
await cit_tools.extract_internal_citations(case_law_id=case_law_id, limit=0)
|
||||||
|
|||||||
Reference in New Issue
Block a user