feat(kb): Phase 1 — Israeli National Insurance knowledge base
Adds a searchable KB for חוק הביטוח הלאומי, תקנות הביטוח הלאומי, and חוזרי הביטוח הלאומי. Hybrid search (pgvector cosine + tsvector/trgm) fused with Reciprocal Rank Fusion, exposed to Shira as the `search_insurance_kb` tool. - api/services/kb/: asyncpg pool, Voyage voyage-multilingual-2 client, per-kind chunker (statute-by-section / circular header-aware), ingest pipeline with supersession (old source → superseded_by), hybrid search. - api/routes/admin_kb.py: POST /admin/kb/ingest (multipart), GET /admin/kb/stats. - mcp_server/tools/legal_kb_tools.py: `search_insurance_kb` registered under the `legal` toolset. - pyproject.toml: +asyncpg, +python-docx, +boto3, +python-multipart. Infra (external): pgvector/pgvector:pg16 on the shared PG, insurance_kb DB with hnsw + gin indexes, MinIO bucket insurance-kb, KB_DATABASE_URL in Infisical /espocrm, VOYAGE_API_KEY/VOYAGE_MODEL/KB_DATABASE_URL on the shira-hermes Coolify app. Refs Task Master #2 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
"""Israeli National Insurance knowledge base tools (search)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from api.services.kb import search as kb_search
|
||||
from mcp_server.tools._helpers import fail
|
||||
|
||||
logger = logging.getLogger("shira.tools.legal_kb")
|
||||
|
||||
_KIND_HEBREW = {
|
||||
"law": "חוק",
|
||||
"regulation": "תקנה",
|
||||
"circular": "חוזר",
|
||||
}
|
||||
|
||||
|
||||
def _format_hits(hits: list[dict]) -> str:
|
||||
if not hits:
|
||||
return "לא נמצאו תוצאות רלוונטיות בבסיס הידע."
|
||||
|
||||
lines = [f"נמצאו {len(hits)} קטעים רלוונטיים:"]
|
||||
for i, h in enumerate(hits, 1):
|
||||
kind_he = _KIND_HEBREW.get(h["kind"], h["kind"])
|
||||
ident = f" {h['identifier']}" if h.get("identifier") else ""
|
||||
ref = h.get("section_ref") or h.get("heading_path") or ""
|
||||
header = f"{i}. [{kind_he}{ident}] {h['title']}"
|
||||
if ref:
|
||||
header += f" — {ref}"
|
||||
lines.append(header)
|
||||
content = (h.get("content") or "").strip()
|
||||
if len(content) > 900:
|
||||
content = content[:900] + "…"
|
||||
lines.append(content)
|
||||
if h.get("source_url"):
|
||||
lines.append(f"מקור: {h['source_url']}")
|
||||
lines.append("")
|
||||
return "\n".join(lines).rstrip()
|
||||
|
||||
|
||||
def register_legal_kb_tools(tools: dict) -> None:
|
||||
async def search_insurance_kb(
|
||||
query: str,
|
||||
kind: str = "any",
|
||||
top_k: int = 8,
|
||||
) -> str:
|
||||
try:
|
||||
if kind not in ("law", "regulation", "circular", "any"):
|
||||
kind = "any"
|
||||
top_k = max(1, min(int(top_k or 8), 15))
|
||||
hits = await kb_search.search(query=query, kind=kind, top_k=top_k)
|
||||
return _format_hits(hits)
|
||||
except Exception as e:
|
||||
logger.exception("[search_insurance_kb] failed")
|
||||
return fail(f"שגיאה בחיפוש בבסיס הידע: {e}")
|
||||
|
||||
tools["search_insurance_kb"] = {
|
||||
"description": (
|
||||
"חיפוש בבסיס הידע של הביטוח הלאומי — חוק הביטוח הלאומי, תקנות הביטוח "
|
||||
"הלאומי וחוזרי הביטוח הלאומי. מחזיר קטעים רלוונטיים עם ציטוט מדויק "
|
||||
"(סעיף/תקנה/מס' חוזר). יש להשתמש בכלי לפני טענה משפטית מבוססת-מקור, "
|
||||
"ולצטט את ה-section_ref המדויק בתשובה. החיפוש משלב דמיון סמנטי וחיפוש "
|
||||
"מילולי (hybrid)."
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "שאילתת חיפוש בעברית — ניסוח חופשי או מונחים מדויקים.",
|
||||
},
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"enum": ["law", "regulation", "circular", "any"],
|
||||
"description": "סינון לפי סוג מקור: law=חוק, regulation=תקנה, circular=חוזר, any=הכל (ברירת מחדל).",
|
||||
},
|
||||
"top_k": {
|
||||
"type": "integer",
|
||||
"description": "מספר קטעים להחזיר (1–15, ברירת מחדל 8).",
|
||||
},
|
||||
},
|
||||
"required": ["query"],
|
||||
},
|
||||
"handler": search_insurance_kb,
|
||||
}
|
||||
Reference in New Issue
Block a user