feat(kb): bulk-fetch תקנות הביטוח הלאומי from Wikisource

Extends scripts/fetch_national_insurance_law.py with a --regulations mode
that pulls all canonical (non-redirect) תקנות הביטוח הלאומי pages from the
MediaWiki API and writes <slug>.txt + <slug>.txt.meta.json pairs ready for
the existing scan-inbox cron.

Used to seed the live insurance KB with 92 regulations (~1,500 chunks,
~22MB Postgres) on dev. Wikisource's redirect graph dedups spelling
variants (קיצבה/קצבה, etc.) for free.

Bumps version to 0.2.0.

Refs Task Master #3

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-25 20:43:31 +00:00
parent bb23b8a120
commit 83a3f63a37
3 changed files with 134 additions and 8 deletions
+15 -3
View File
@@ -24,13 +24,25 @@
"priority": "high", "priority": "high",
"subtasks": [], "subtasks": [],
"updatedAt": "2026-04-21T00:00:00.000Z" "updatedAt": "2026-04-21T00:00:00.000Z"
},
{
"id": "3",
"title": "Add תקנות הביטוח הלאומי (~100 regulations) from Wikisource to KB",
"description": "Extend scripts/fetch_national_insurance_law.py to bulk-fetch ~100 regulation pages from Hebrew Wikisource via MediaWiki API. Output text + sidecar JSON pairs to /tmp/regulations/. Upload to MinIO inbox/regulation/ for the existing scan-inbox cron to embed via Voyage. Estimated 30-40MB Postgres growth, ~$0.36 embedding cost. Sample 3 first (small/medium/large), verify chunker output, then bulk.",
"details": "",
"testStrategy": "",
"status": "done",
"dependencies": [],
"priority": "high",
"subtasks": [],
"updatedAt": "2026-04-25T20:39:11.905Z"
} }
], ],
"metadata": { "metadata": {
"version": "1.0.0", "version": "1.0.0",
"lastModified": "2026-04-21T00:00:00.000Z", "lastModified": "2026-04-25T20:39:11.905Z",
"taskCount": 2, "taskCount": 3,
"completedCount": 0, "completedCount": 1,
"tags": [ "tags": [
"master" "master"
] ]
+1 -1
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "shira-hermes" name = "shira-hermes"
version = "0.1.0" version = "0.2.0"
description = "Shira AI Assistant — Hermes Agent backend for EspoCRM SmartAssistant" description = "Shira AI Assistant — Hermes Agent backend for EspoCRM SmartAssistant"
requires-python = ">=3.11" requires-python = ">=3.11"
dependencies = [ dependencies = [
+117 -3
View File
@@ -1,17 +1,28 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Fetch חוק הביטוח הלאומי from Hebrew Wikisource and convert to plain text """Fetch חוק הביטוח הלאומי and its תקנות from Hebrew Wikisource and convert to
with the headers the shira-hermes chunker expects (חלק / פרק / סימן / סעיף).""" plain text with the headers the shira-hermes chunker expects (חלק / פרק /
סימן / סעיף).
Modes:
(default) fetch the parent law (חוק הביטוח הלאומי) → /tmp/national_insurance_law.txt
--regulations fetch all תקנות הביטוח הלאומי canonical pages (~92, redirects skipped)
→ /tmp/regulations/<slug>.txt + <slug>.txt.meta.json
"""
from __future__ import annotations from __future__ import annotations
import argparse
import json import json
import os
import re import re
import sys import sys
import time
import urllib.request import urllib.request
import urllib.parse import urllib.parse
WIKI_PAGE = "חוק_הביטוח_הלאומי" WIKI_PAGE = "חוק_הביטוח_הלאומי"
API = "https://he.wikisource.org/w/api.php" API = "https://he.wikisource.org/w/api.php"
REGULATIONS_PREFIX = "תקנות הביטוח הלאומי"
def fetch_wikitext(page: str) -> str: def fetch_wikitext(page: str) -> str:
@@ -217,8 +228,111 @@ def _cleanup(text: str) -> str:
return text.strip() return text.strip()
def fetch_regulation_titles() -> list[str]:
"""Query MediaWiki API for canonical (non-redirect) תקנות הביטוח הלאומי pages.
Wikisource keeps duplicate-spelling pages (e.g. קיצבה/קצבה) as redirects to
the modern canonical title. Filtering apfilterredir=nonredirects gives us
the deduped set without having to maintain a hand-curated list.
"""
titles: list[str] = []
apcontinue: str | None = None
while True:
params = {
"action": "query",
"list": "allpages",
"apprefix": REGULATIONS_PREFIX,
"apnamespace": "0",
"apfilterredir": "nonredirects",
"aplimit": "500",
"format": "json",
}
if apcontinue:
params["apcontinue"] = apcontinue
url = API + "?" + urllib.parse.urlencode(params)
req = urllib.request.Request(url, headers={"User-Agent": "shira-kb/1.0"})
with urllib.request.urlopen(req) as r:
data = json.load(r)
titles.extend(p["title"] for p in data["query"]["allpages"])
cont = data.get("continue", {}).get("apcontinue")
if not cont:
break
apcontinue = cont
return titles
_SLUG_DROP = re.compile(r"[()\[\]{}'\"־–—]") # parens, brackets, quotes, hyphens
_SLUG_WS = re.compile(r"[\s\-]+")
def _slug(title: str) -> str:
"""Convert a Hebrew title into a safe filename — Hebrew letters preserved.
Strips parens/brackets/quotes (problematic in shells and S3 console URLs),
collapses whitespace and hyphens to a single hyphen.
"""
s = _SLUG_DROP.sub(" ", title)
s = _SLUG_WS.sub("-", s.strip())
return s
def fetch_one_regulation(title: str, out_dir: str) -> dict:
"""Fetch a single regulation, expand templates, clean, write text + sidecar."""
wt = fetch_wikitext(title)
expanded = _expand_templates(wt)
clean = _cleanup(expanded)
slug = _slug(title)
txt_path = os.path.join(out_dir, f"{slug}.txt")
meta_path = txt_path + ".meta.json"
with open(txt_path, "w", encoding="utf-8") as f:
f.write(clean)
sidecar = {
"title": title,
"identifier": title,
"source_url": f"https://he.wikisource.org/wiki/{urllib.parse.quote(title.replace(' ', '_'))}",
}
with open(meta_path, "w", encoding="utf-8") as f:
json.dump(sidecar, f, ensure_ascii=False, indent=2)
return {"title": title, "slug": slug, "raw_chars": len(wt), "clean_chars": len(clean)}
def fetch_all_regulations(out_dir: str) -> int:
"""Fetch every canonical תקנות הביטוח הלאומי page into out_dir."""
os.makedirs(out_dir, exist_ok=True)
titles = fetch_regulation_titles()
print(f"[regulations] {len(titles)} canonical pages", file=sys.stderr)
total_clean = 0
for i, title in enumerate(titles, 1):
try:
r = fetch_one_regulation(title, out_dir)
total_clean += r["clean_chars"]
print(
f"[{i:3d}/{len(titles)}] {r['slug']:.<70s} "
f"raw={r['raw_chars']:>7,} clean={r['clean_chars']:>7,}",
file=sys.stderr,
)
except Exception as e:
print(f"[{i:3d}/{len(titles)}] FAILED {title}: {e}", file=sys.stderr)
# Be polite to Wikisource — 50ms between requests = ~20 req/s, well under any limit.
time.sleep(0.05)
print(f"[done] total clean text: {total_clean:,} chars across {len(titles)} regulations",
file=sys.stderr)
return 0
def main() -> int: def main() -> int:
out_path = sys.argv[1] if len(sys.argv) > 1 else "/tmp/national_insurance_law.txt" parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("--regulations", action="store_true",
help="fetch all תקנות (default: fetch the parent law only)")
parser.add_argument("out", nargs="?", default=None,
help="output path (default: /tmp/national_insurance_law.txt for law, /tmp/regulations/ for --regulations)")
args = parser.parse_args()
if args.regulations:
out_dir = args.out or "/tmp/regulations"
return fetch_all_regulations(out_dir)
out_path = args.out or "/tmp/national_insurance_law.txt"
wt = fetch_wikitext(WIKI_PAGE) wt = fetch_wikitext(WIKI_PAGE)
print(f"[fetch] raw wikitext: {len(wt):,} chars", file=sys.stderr) print(f"[fetch] raw wikitext: {len(wt):,} chars", file=sys.stderr)
expanded = _expand_templates(wt) expanded = _expand_templates(wt)