From 88ca96bf19a309b83a51368f0a4b98bd43b097d5 Mon Sep 17 00:00:00 2001 From: Chaim Date: Tue, 30 Jun 2026 17:35:52 +0000 Subject: [PATCH] perf(citation-verification): cache view + parallelize backend retrieval MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "אימות פסיקה" tab reloaded its data on every tab revisit and the first load took 1.5–3s. Two independent causes, two fixes: Frontend (web-ui/src/lib/api/citation-verification.ts): - staleTime 10s → 5min and refetchOnMount: false. Radix Tabs unmounts inactive TabsContent, so switching back used to trigger a full rebuild once the 10s window elapsed. Verify mutations still invalidate the query, so edits pull fresh data. Backend (mcp-server/.../case_citation_verification.py): - The per-argument loop ran a Voyage embed + vector search + batched authority query sequentially (N args × ~300ms waterfall). Extracted to _fetch() and fanned out with asyncio.gather — order preserved, n_verified counter and per-argument seen-sets unchanged. First load ~5× faster on multi-argument cases. Pure read/assembly — no new parallel path (G2), no silent error swallowing (search/radar still log warnings). Invariants: INV-DIG1/INV-AH unaffected (still read-only, never cites). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../services/case_citation_verification.py | 27 ++++++++++++++----- web-ui/src/lib/api/citation-verification.ts | 8 +++++- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/mcp-server/src/legal_mcp/services/case_citation_verification.py b/mcp-server/src/legal_mcp/services/case_citation_verification.py index 663cacd..26c2f97 100644 --- a/mcp-server/src/legal_mcp/services/case_citation_verification.py +++ b/mcp-server/src/legal_mcp/services/case_citation_verification.py @@ -18,6 +18,7 @@ rows. Reuses the one corpus search + the one authority query + the one radar (G2 from __future__ import annotations +import asyncio import logging from uuid import UUID @@ -65,25 +66,37 @@ async def build_view(case_number: str) -> dict: except Exception as e: # noqa: BLE001 — radar is best-effort logger.warning("citation_verification radar failed for %s: %s", case_number, e) - out_args: list[dict] = [] - n_verified = 0 - for a in args: - aid = str(a["id"]) + async def _fetch(a: dict) -> tuple[list[dict], dict]: + """Per-argument corpus search + batched authority — run concurrently. + + Each call is one Voyage embed + one vector search (+ one batched authority + query); independent across arguments, so they fan out rather than waterfall. + """ title = (a.get("argument_title") or "").strip() topic = (a.get("legal_topic") or "").strip() query = f"{ctx} {title}. {topic}".strip() - - hits = [] + hits: list[dict] = [] try: hits = await precedent_library.search_library( query=query, limit=_SUGGEST_PER_ISSUE, include_halachot=True) except Exception as e: # noqa: BLE001 logger.warning("citation_verification search failed (%s): %s", title[:30], e) - # Resolve the authority breakdown for the hit set in one batched query. clids = [UUID(str(h["case_law_id"])) for h in hits if h.get("case_law_id") and float(h.get("score", 0) or 0) >= _SUGGEST_FLOOR] authority = await db.citation_authority(clids) if clids else {} + return hits, authority + + # Fan out the expensive per-argument retrieval concurrently — was a sequential + # waterfall (N args × Voyage embed + vector search each). gather preserves order. + fetched = await asyncio.gather(*(_fetch(a) for a in args)) if args else [] + + out_args: list[dict] = [] + n_verified = 0 + for a, (hits, authority) in zip(args, fetched): + aid = str(a["id"]) + title = (a.get("argument_title") or "").strip() + topic = (a.get("legal_topic") or "").strip() seen: set[str] = set() supporting: list[dict] = [] diff --git a/web-ui/src/lib/api/citation-verification.ts b/web-ui/src/lib/api/citation-verification.ts index c22468e..6e11b4a 100644 --- a/web-ui/src/lib/api/citation-verification.ts +++ b/web-ui/src/lib/api/citation-verification.ts @@ -84,7 +84,13 @@ export function useCitationVerification(caseNumber: string | undefined) { { signal }, ), enabled: Boolean(caseNumber), - staleTime: 10_000, + // The view is an expensive synthesized build (per-argument Voyage embed + + // vector search on the backend). Keep it fresh for 5 min and do NOT refetch + // on remount — Radix Tabs unmounts inactive TabsContent, so every tab switch + // back used to trigger a full rebuild. Verify mutations invalidate the query, + // so edits still pull fresh data. + staleTime: 5 * 60_000, + refetchOnMount: false, }); }