feat: Stage C — RAG advanced (#33, #47, #48, #49, #50, #51)
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m35s

Six independent sub-tasks dispatched in parallel; aggregated here.

## #33 — Hide case_name column
library-list-panel.tsx: `<TableHead>` + `<TableCell>` for "שם"
get `className="hidden"` in both Court and Committee row variants.
DB column preserved for future use.

## #47 — Audit script periodic
New scripts/audit_corpus_integrity.py — 3 SQL checks (external+ערר
prefix, internal missing chair/district, cases.practice_area enum)
+ CEO wakeup on violations + cron `0 7 * * *`. First run: 0 issues.

## #48 — Parent-doc retrieval (gated, default off)
Schema V17: precedent_chunks.parent_chunk_id + chunk_role
('child'|'parent'). New chunker.chunk_document_hierarchical() —
section-aware parents (~1500 tokens) containing ~5 overlapping
children (~300 tokens each). New db.store_precedent_chunks_hierarchical
two-pass writer. Search SQL (semantic + lexical) LEFT-JOIN parent and
swap content + dedupe by parent_chunk_id when flag on. Toggle:
PARENT_DOC_RETRIEVAL_ENABLED + PARENT_DOC_{CHILD,PARENT}_SIZE_TOKENS.
Backfill ~3min and ~$0.20 — deferred to follow-up.

## #49 — Multimodal backfill
New scripts/backfill_multimodal_precedents.py with token-matching
case_number ↔ source files (PDF + DOCX via PyMuPDF). Ran in container:
26 precedents embedded, 503 pages, $0.21, 0 errors. precedent_image_embeddings
grew 3 → 29 rows. 44 remaining are style_corpus-migrated rows (no
source file on disk) — will catch up when re-uploaded.

## #50 — Closed-loop feedback + nDCG
Schema V18: search_logs + search_relevance_feedback. New telemetry.py
with fire-and-forget log_search_bg (p50 = 0.002ms — zero overhead) +
auto-infer_relevance_from_citations (reads case drafts → marks score=3
when cited precedent appears in past search top-K). Hooks added to 5
search paths. scripts/compute_ndcg.py for aggregation. Two admin API
endpoints (GET /api/admin/rag-metrics + POST .../infer). Dashboard UI
deferred — API is enough for now.

## #51 — Halacha quality monitoring
New scripts/monitor_halacha_quality.py — baseline avg confidence
(trusted=0.849, all=0.833, pending=0.694) with rolling window drift
detection. Default 5% threshold. Exits non-zero on alert for cron
integration. Recommended: `0 8 * * 1` weekly Mon 8am.

## Bonus: 230 unlinked citations → missing_precedents
Bulk-imported 230 distinct unlinked citations from
precedent_internal_citations to missing_precedents.status='open',
party='committee', with notes listing source citers. Top candidate:
ע"א 3213/97 (cited 5x). Total open missing_precedents now 237.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-26 11:26:52 +00:00
parent 3a05e30c8d
commit 2aee398b4a
15 changed files with 2493 additions and 57 deletions

View File

@@ -18,9 +18,10 @@ the chair approves them — per project review policy.
from __future__ import annotations
import json
import time
from uuid import UUID
from legal_mcp.services import db, precedent_library
from legal_mcp.services import db, precedent_library, telemetry
def _ok(payload) -> str:
@@ -260,8 +261,10 @@ async def search_precedent_library(
"""
if not query or len(query.strip()) < 2:
return json.dumps([], ensure_ascii=False)
q = query.strip()
t0 = time.perf_counter()
results = await precedent_library.search_library(
query=query.strip(),
query=q,
practice_area=practice_area,
court=court,
precedent_level=precedent_level,
@@ -271,6 +274,15 @@ async def search_precedent_library(
limit=limit,
include_halachot=include_halachot,
)
elapsed_ms = int((time.perf_counter() - t0) * 1000)
telemetry.log_search_bg(
search_type="precedent_library",
query=q,
results=results,
duration_ms=elapsed_ms,
practice_area=practice_area or None,
user_agent="unknown",
)
return _ok(results)

View File

@@ -4,9 +4,10 @@ from __future__ import annotations
import json
import logging
import time
from uuid import UUID
from legal_mcp.services import db, embeddings, hybrid_search
from legal_mcp.services import db, embeddings, hybrid_search, telemetry
logger = logging.getLogger(__name__)
@@ -30,11 +31,16 @@ async def search_decisions(
case_number: אם סופק, ה-practice_area/subtype יוסקו אוטומטית מהתיק
"""
# Auto-resolve practice_area from case_number if available
resolved_case_id: UUID | None = None
if case_number and not practice_area:
case = await db.get_case_by_number(case_number)
if case:
practice_area = case.get("practice_area") or ""
appeal_subtype = appeal_subtype or (case.get("appeal_subtype") or "")
try:
resolved_case_id = UUID(case["id"])
except (KeyError, ValueError, TypeError):
resolved_case_id = None
if not practice_area:
logger.warning(
@@ -43,6 +49,7 @@ async def search_decisions(
)
query_emb = await embeddings.embed_query(query)
t0 = time.perf_counter()
results = await hybrid_search.search_documents_hybrid(
query=query,
query_text_embedding=query_emb,
@@ -51,6 +58,16 @@ async def search_decisions(
practice_area=practice_area or None,
appeal_subtype=appeal_subtype or None,
)
elapsed_ms = int((time.perf_counter() - t0) * 1000)
telemetry.log_search_bg(
search_type="decisions",
query=query,
results=results,
duration_ms=elapsed_ms,
practice_area=practice_area or None,
case_id=resolved_case_id,
user_agent="unknown",
)
if not results:
return "לא נמצאו תוצאות."
@@ -87,13 +104,24 @@ async def search_case_documents(
if not case:
return f"תיק {case_number} לא נמצא."
case_uuid = UUID(case["id"])
query_emb = await embeddings.embed_query(query)
# Restricted to case_id — practice_area filter would be redundant.
t0 = time.perf_counter()
results = await hybrid_search.search_documents_hybrid(
query=query,
query_text_embedding=query_emb,
limit=limit,
case_id=UUID(case["id"]),
case_id=case_uuid,
)
elapsed_ms = int((time.perf_counter() - t0) * 1000)
telemetry.log_search_bg(
search_type="case_documents",
query=query,
results=results,
duration_ms=elapsed_ms,
case_id=case_uuid,
user_agent="unknown",
)
if not results:
@@ -130,11 +158,16 @@ async def find_similar_cases(
appeal_subtype: סוג ערר לסינון
case_number: אם סופק, ה-practice_area/subtype יוסקו אוטומטית מהתיק
"""
resolved_case_id: UUID | None = None
if case_number and not practice_area:
case = await db.get_case_by_number(case_number)
if case:
practice_area = case.get("practice_area") or ""
appeal_subtype = appeal_subtype or (case.get("appeal_subtype") or "")
try:
resolved_case_id = UUID(case["id"])
except (KeyError, ValueError, TypeError):
resolved_case_id = None
if not practice_area:
logger.warning(
@@ -145,6 +178,7 @@ async def find_similar_cases(
query_emb = await embeddings.embed_query(description)
# Even with rerank we ask for ``limit*3`` so the dedup-by-case
# step downstream still has enough rows to pick the best per case.
t0 = time.perf_counter()
results = await hybrid_search.search_documents_hybrid(
query=description,
query_text_embedding=query_emb,
@@ -152,6 +186,16 @@ async def find_similar_cases(
practice_area=practice_area or None,
appeal_subtype=appeal_subtype or None,
)
elapsed_ms = int((time.perf_counter() - t0) * 1000)
telemetry.log_search_bg(
search_type="similar_cases",
query=description,
results=results,
duration_ms=elapsed_ms,
practice_area=practice_area or None,
case_id=resolved_case_id,
user_agent="unknown",
)
if not results:
return "לא נמצאו תיקים דומים."
@@ -213,6 +257,7 @@ async def search_internal_decisions(
# expansion more useful.
primary_limit = limit if not include_cited_by else max(limit, limit * 2)
t0 = time.perf_counter()
results = await int_svc.search_internal(
query,
practice_area=practice_area,
@@ -222,6 +267,15 @@ async def search_internal_decisions(
limit=primary_limit,
include_halachot=include_halachot,
)
elapsed_ms = int((time.perf_counter() - t0) * 1000)
telemetry.log_search_bg(
search_type="internal_decisions",
query=query,
results=results,
duration_ms=elapsed_ms,
practice_area=practice_area or None,
user_agent="unknown",
)
if not results:
return "לא נמצאו החלטות ועדת ערר רלוונטיות."