Legal-i Claude Opus 4.7 (1M context) commited on
Commit
85b0079
·
1 Parent(s): 1d6f0f3

feat(Day 49): /v1/doctrines/classify endpoint for topic-dossier UX

Browse files

New POST endpoint that takes a free-text query and returns top-k
matching doctrines with metadata (name_he, anchor_case, anchor_year,
statute, domain, score, pending_lawyer_review flag).

Powers the frontend topic-dossier panel that appears inline with
lawyer-ask results — user sees related doctrines without navigating
away. Uses the existing classify_doctrines() + load_doctrine_catalog()
internals; no new business logic.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Files changed (1) hide show
  1. tau_rag/api/fastapi_app.py +48 -0
tau_rag/api/fastapi_app.py CHANGED
@@ -22537,6 +22537,54 @@ def admin_funnel(request: Request, hours: int = 24): # type: ignore
22537
  # `anchor_case`, `n_leading_cases` — not the full schema (elements,
22538
  # exceptions, keywords stay internal).
22539
  # ──────────────────────────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22540
  @app.get("/v1/doctrines/catalog")
22541
  def doctrines_catalog_public(): # type: ignore
22542
  """Public read-only summary of the curated doctrine catalog."""
 
22537
  # `anchor_case`, `n_leading_cases` — not the full schema (elements,
22538
  # exceptions, keywords stay internal).
22539
  # ──────────────────────────────────────────────────────────────────────
22540
+ class _DoctrineClassifyRequest(BaseModel): # type: ignore
22541
+ text: str
22542
+ k: int = 3
22543
+ min_score: float = 1.0
22544
+
22545
+
22546
+ @app.post("/v1/doctrines/classify")
22547
+ def doctrines_classify(body: _DoctrineClassifyRequest): # type: ignore
22548
+ """Day 49 — classify a free-text query against the doctrine catalog.
22549
+
22550
+ Returns the top-k matching doctrines with their metadata so the
22551
+ frontend can render a "related doctrines" panel inline with the
22552
+ lawyer-ask result (topic-dossier UX).
22553
+ """
22554
+ if not (body.text or "").strip():
22555
+ return {"ok": True, "matches": [], "n_total": 0}
22556
+ try:
22557
+ from ..intelligence.doctrine_classifier import (
22558
+ classify_doctrines, load_doctrine_catalog,
22559
+ )
22560
+ cat = load_doctrine_catalog()
22561
+ idx = {d["id"]: d for d in cat.get("doctrines", [])}
22562
+ matches = classify_doctrines(body.text, k=body.k,
22563
+ min_score=body.min_score)
22564
+ out = []
22565
+ for m in matches:
22566
+ doc = idx.get(m.doctrine_id, {})
22567
+ leading = (doc.get("leading_cases") or [])[:1]
22568
+ anchor_case = leading[0].get("citation") if leading else None
22569
+ anchor_year = leading[0].get("year") if leading else None
22570
+ out.append({
22571
+ "id": m.doctrine_id,
22572
+ "name_he": m.name_he,
22573
+ "domain": m.domain,
22574
+ "score": round(m.score, 2),
22575
+ "anchor_case": anchor_case,
22576
+ "anchor_year": anchor_year,
22577
+ "statute": ((doc.get("statute_refs") or [{}])[0]
22578
+ .get("law") if doc.get("statute_refs") else None),
22579
+ "pending_review": bool(doc.get("_pending_lawyer_review")),
22580
+ })
22581
+ return {"ok": True, "matches": out, "n_total": len(out)}
22582
+ except Exception as e:
22583
+ return JSONResponse(status_code=500, content={
22584
+ "ok": False, "error": f"{type(e).__name__}: {e}"
22585
+ })
22586
+
22587
+
22588
  @app.get("/v1/doctrines/catalog")
22589
  def doctrines_catalog_public(): # type: ignore
22590
  """Public read-only summary of the curated doctrine catalog."""