The style-exemplar corpus (channel B — the writer's block-level retrieval of Dafna's real prose) was FROZEN at the one-time seed backfill: new finals were enrolled into style_corpus but never broken into exemplars, so the richest style channel never grew (8137/8126/8174 had 0 exemplars). The writer kept retrieving only March–April seed paragraphs no matter how many finals were signed. Extract the per-decision exemplar logic (section→paragraph→Voyage-embed→replace) into a shared service `legal_mcp.services.style_exemplars.extract_and_store` — the SINGLE implementation now used by BOTH the one-time backfill and the live enrollment path (G2; no parallel extractor). `_enroll_final_in_library` calls it on every final upload (source='internal_committee', the same source the writer's search_style_exemplars reads). Voyage embeds over REST → container-safe; best-effort, surfaced in the upload response, never fails the upload. Effect: every signed final now grows the exemplar corpus, so the writer's block-level style retrieval improves with each decision — the core "learn from every decision" fix for channel B. Path A (style_distance_history) will track whether the larger exemplar pool reduces style-distance over time. Invariants: G2 (one extraction path shared by backfill + enroll), INV-LRN5 (style/structure prose only — substance routes elsewhere), INV-LRN4 (the draft↔final loop now feeds the exemplar channel, not just the lesson channel). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
97 lines
3.7 KiB
Python
97 lines
3.7 KiB
Python
#!/usr/bin/env python3
|
|
"""T1 — אכלוס style_exemplars מקורפוס דפנה (style_corpus + internal_committee).
|
|
|
|
מפצל כל החלטה של דפנה לסעיפים (chunker._split_into_sections), ומכל סעיף לפסקאות,
|
|
מטמיע (Voyage) ושומר ב-style_exemplars עם section/outcome/practice_area — כדי
|
|
שהכותב יוכל לאחזר פסקאות-בלוק אמיתיות של דפנה בזמן כתיבה (T2/T3).
|
|
|
|
מקור-סגנון בלבד (INV-LRN5) — לא מהות. אידמפוטנטי: מנקה לכל decision לפני הכנסה.
|
|
|
|
שימוש:
|
|
python3 scripts/backfill_style_exemplars.py # dry-run (סופר בלבד)
|
|
python3 scripts/backfill_style_exemplars.py --apply # מטמיע ושומר
|
|
|
|
דורש POSTGRES_URL + מפתח Voyage בסביבה (כמו שאר ה-MCP).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import asyncio
|
|
import logging
|
|
|
|
from legal_mcp.services import db
|
|
from legal_mcp.services import style_exemplars as sx
|
|
|
|
logging.basicConfig(level=logging.INFO, format="%(message)s")
|
|
log = logging.getLogger("backfill_exemplars")
|
|
|
|
# Section mapping + paragraph splitting now live in the shared service
|
|
# (legal_mcp.services.style_exemplars) so the backfill and the live
|
|
# final-enrollment path use ONE extraction implementation (G2).
|
|
|
|
|
|
async def _gather_sources() -> list[dict]:
|
|
"""All of Dafna's decisions: style_corpus + internal_committee (chair דפנה)."""
|
|
pool = await db.get_pool()
|
|
rows: list[dict] = []
|
|
async with pool.acquire() as conn:
|
|
sc = await conn.fetch(
|
|
"SELECT decision_number, full_text, outcome, practice_area "
|
|
"FROM style_corpus WHERE full_text <> ''"
|
|
)
|
|
for r in sc:
|
|
rows.append({
|
|
"decision_number": r["decision_number"] or "",
|
|
"source": "style_corpus",
|
|
"full_text": r["full_text"],
|
|
"outcome": r["outcome"] or "",
|
|
"practice_area": r["practice_area"] or "",
|
|
})
|
|
ic = await conn.fetch(
|
|
"SELECT case_number, full_text, practice_area FROM case_law "
|
|
"WHERE source_kind = 'internal_committee' AND coalesce(chair_name,'') LIKE '%דפנה%' "
|
|
"AND coalesce(full_text,'') <> ''"
|
|
)
|
|
for r in ic:
|
|
rows.append({
|
|
"decision_number": r["case_number"] or "",
|
|
"source": "internal_committee",
|
|
"full_text": r["full_text"],
|
|
"outcome": "",
|
|
"practice_area": r["practice_area"] or "",
|
|
})
|
|
return rows
|
|
|
|
|
|
async def main(apply: bool) -> None:
|
|
sources = await _gather_sources()
|
|
log.info("מקורות: %d החלטות של דפנה (style_corpus + internal_committee)", len(sources))
|
|
|
|
total_paras = 0
|
|
for src in sources:
|
|
n = len(sx.units_for(src["full_text"]))
|
|
if not n:
|
|
continue
|
|
total_paras += n
|
|
log.info(" %-14s %-16s → %d פסקאות", src["source"], src["decision_number"], n)
|
|
if not apply:
|
|
continue
|
|
await sx.extract_and_store(
|
|
decision_number=src["decision_number"], source=src["source"],
|
|
full_text=src["full_text"], practice_area=src["practice_area"],
|
|
outcome=src["outcome"],
|
|
)
|
|
|
|
if apply:
|
|
cov = await db.count_style_exemplars()
|
|
log.info("הושלם. style_exemplars: %s", cov)
|
|
else:
|
|
log.info("dry-run: %d פסקאות יוטמעו. הרץ --apply לביצוע.", total_paras)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--apply", action="store_true", help="embed + insert (default: dry-run)")
|
|
args = ap.parse_args()
|
|
asyncio.run(main(args.apply))
|