feat(kb): add 'caselaw' (פסיקה) as a fourth kind
Court rulings are a distinct content type — long discussion / reasoning, no enumerated statutory hierarchy, case identifier instead of statute id — so they get their own kind alongside law/regulation/circular rather than being shoehorned into one of the existing three. Migration 003 alters the CHECK constraints on kb_source.kind and kb_ingest_job.kind to include 'caselaw'. The chunker uses the circulars path for caselaw too (paragraph + size split, no statutory section regex); the case identifier is captured at upload time in kb_source.identifier. Updates Literal types in ingest_source, admin_kb upload, and the kb_public SearchRequest. _KINDS in s3.py grows to four so failed/ processed/inbox listings include caselaw subdirs. Refs Task Master #18 (espocrm-extensions/KnowledgeBase) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -37,7 +37,7 @@ def _verify_admin(request: Request) -> None:
|
||||
@router.post("/ingest")
|
||||
async def ingest(
|
||||
request: Request,
|
||||
kind: Literal["law", "regulation", "circular"] = Form(...),
|
||||
kind: Literal["law", "regulation", "circular", "caselaw"] = Form(...),
|
||||
title: str = Form(...),
|
||||
identifier: str | None = Form(None),
|
||||
published_at: str | None = Form(None),
|
||||
@@ -226,7 +226,7 @@ _MAX_UPLOAD_BYTES = 50 * 1024 * 1024
|
||||
@router.post("/upload")
|
||||
async def upload(
|
||||
request: Request,
|
||||
kind: Literal["law", "regulation", "circular"] = Form(...),
|
||||
kind: Literal["law", "regulation", "circular", "caselaw"] = Form(...),
|
||||
topic_id: int = Form(...),
|
||||
title: str | None = Form(None),
|
||||
identifier: str | None = Form(None),
|
||||
|
||||
@@ -36,7 +36,7 @@ def _verify_auth(request: Request) -> None:
|
||||
|
||||
class SearchRequest(BaseModel):
|
||||
query: str = Field(..., min_length=1, max_length=500)
|
||||
kind: Literal["any", "law", "regulation", "circular"] = "any"
|
||||
kind: Literal["any", "law", "regulation", "circular", "caselaw"] = "any"
|
||||
top_k: int = Field(8, ge=1, le=15)
|
||||
# Multi-query expansion: ask the LLM for alternative phrasings, retrieve
|
||||
# for each, merge, then rerank against the original query. Recovers
|
||||
|
||||
@@ -396,10 +396,16 @@ def chunk_circular(
|
||||
return chunks
|
||||
|
||||
|
||||
def chunk(text: str, kind: Literal["law", "regulation", "circular"]) -> list[dict]:
|
||||
def chunk(text: str, kind: Literal["law", "regulation", "circular", "caselaw"]) -> list[dict]:
|
||||
if kind in ("law", "regulation"):
|
||||
chunks = chunk_statute(text)
|
||||
else:
|
||||
# circulars and caselaw share the same fallback: no enumerated
|
||||
# statutory hierarchy, so we split by paragraph + size. The
|
||||
# caselaw section_ref ends up empty (the chunker can't infer a
|
||||
# case identifier from body text reliably) — the user supplies
|
||||
# the case identifier via the upload form's `identifier` field
|
||||
# and it's stored on the parent kb_source row instead.
|
||||
chunks = chunk_circular(text)
|
||||
# _split_oversized uses page markers still embedded in content to derive
|
||||
# per-piece page numbers; _as_dicts then strips markers and emits the
|
||||
|
||||
@@ -136,7 +136,7 @@ def parse(data: bytes, filename: str) -> str:
|
||||
|
||||
async def ingest_source(
|
||||
*,
|
||||
kind: Literal["law", "regulation", "circular"],
|
||||
kind: Literal["law", "regulation", "circular", "caselaw"],
|
||||
title: str,
|
||||
identifier: str | None,
|
||||
content: bytes,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"""MinIO/S3 helpers for the insurance-kb bucket.
|
||||
|
||||
Layout in the bucket:
|
||||
inbox/{law,regulation,circular}/<file>
|
||||
processed/{law,regulation,circular}/<file>
|
||||
failed/{law,regulation,circular}/<file>
|
||||
inbox/{law,regulation,circular,caselaw}/<file>
|
||||
processed/{law,regulation,circular,caselaw}/<file>
|
||||
failed/{law,regulation,circular,caselaw}/<file>
|
||||
|
||||
After a successful ingest, the file is moved from inbox/* to processed/*.
|
||||
On failure it is moved to failed/*.
|
||||
@@ -20,7 +20,7 @@ from botocore.client import Config as BotoConfig
|
||||
|
||||
logger = logging.getLogger("shira.kb.s3")
|
||||
|
||||
_KINDS = ("law", "regulation", "circular")
|
||||
_KINDS = ("law", "regulation", "circular", "caselaw")
|
||||
|
||||
|
||||
def _client():
|
||||
|
||||
@@ -34,7 +34,7 @@ _EXPANSION_TIMEOUT_S = 12.0
|
||||
|
||||
async def _retrieve_rrf(
|
||||
query: str,
|
||||
kind: Literal["law", "regulation", "circular", "any"],
|
||||
kind: Literal["law", "regulation", "circular", "caselaw", "any"],
|
||||
topic_id: int | None = None,
|
||||
) -> list[dict]:
|
||||
"""Run vector + lexical retrieval, fuse with RRF. No rerank, no top_k cut.
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
-- Migration 003: add 'caselaw' (פסיקה) as a fourth content kind.
|
||||
--
|
||||
-- Task Master #18 (espocrm-extensions/KnowledgeBase). Until now the KB
|
||||
-- holds laws / regulations / circulars only. Court rulings (פסיקה) are
|
||||
-- a distinct content type — different structure (no numbered sections,
|
||||
-- long discussion / reasoning, case identifier instead of statute id) —
|
||||
-- so we surface it as a separate `kind` rather than abusing one of the
|
||||
-- existing three.
|
||||
--
|
||||
-- The CHECK constraints on kb_source.kind and kb_ingest_job.kind are
|
||||
-- recreated with the expanded set. `kb_source.kind` rules every search
|
||||
-- and ask path; `kb_ingest_job.kind` rules the upload form. Both must
|
||||
-- agree.
|
||||
--
|
||||
-- Idempotent — wrapped in a transaction; if the constraint is already
|
||||
-- in the new shape, the DROP is a no-op (IF EXISTS) and the ADD just
|
||||
-- re-asserts.
|
||||
--
|
||||
-- Run as postgres superuser:
|
||||
-- docker exec -i <pg-container> psql -U postgres -d insurance_kb \
|
||||
-- -f /tmp/003_caselaw_kind.sql
|
||||
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE kb_source DROP CONSTRAINT IF EXISTS kb_source_kind_check;
|
||||
ALTER TABLE kb_source
|
||||
ADD CONSTRAINT kb_source_kind_check
|
||||
CHECK (kind IN ('law','regulation','circular','caselaw'));
|
||||
|
||||
ALTER TABLE kb_ingest_job DROP CONSTRAINT IF EXISTS kb_ingest_job_kind_check;
|
||||
ALTER TABLE kb_ingest_job
|
||||
ADD CONSTRAINT kb_ingest_job_kind_check
|
||||
CHECK (kind IN ('law','regulation','circular','caselaw'));
|
||||
|
||||
COMMIT;
|
||||
Reference in New Issue
Block a user