From 8e2cfd602cb99b70062efe3592b719bcc3135d00 Mon Sep 17 00:00:00 2001 From: Chaim Date: Sun, 28 Jun 2026 10:48:46 +0000 Subject: [PATCH] fix(learning): extract precedent metadata inline on final-decision enroll MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../legal_mcp/services/precedent_library.py | 5 +++- web/app.py | 28 +++++++++++++++++-- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/mcp-server/src/legal_mcp/services/precedent_library.py b/mcp-server/src/legal_mcp/services/precedent_library.py index 8de10b2..0cab8d8 100644 --- a/mcp-server/src/legal_mcp/services/precedent_library.py +++ b/mcp-server/src/legal_mcp/services/precedent_library.py @@ -383,7 +383,10 @@ async def reextract_metadata( appeal_subtype, and case_name when it equals the citation). User 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 diff --git a/web/app.py b/web/app.py index a1b141a..1d2a4c6 100644 --- a/web/app.py +++ b/web/app.py @@ -3643,9 +3643,11 @@ async def _enroll_final_in_library( out["error"] = "no final text extracted" return out - # Deterministic metadata from the case record — the Gemini metadata extractor is - # tuned for EXTERNAL rulings and returns no_metadata for internal decisions, so we - # populate proceeding_type / date / tags / summary / citation ourselves (no LLM). + # Deterministic seeds from the case record first — proceeding_type / date / + # citation are identity, not inference, so we set them ourselves (no LLM) and the + # 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 = "ירושלים" proceeding_type = (case.get("proceeding_type") or "ערר").strip() 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: 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. try: await cit_tools.extract_internal_citations(case_law_id=case_law_id, limit=0)