Practice area separation: multi-tenant axis across DB, RAG, and UI
Adds two orthogonal columns — practice_area (top-level legal domain: appeals_committee / national_insurance / labor_law) and appeal_subtype (building_permit / betterment_levy / compensation_197) — denormalized into cases, documents, document_chunks, decisions, and style_corpus so vector searches can filter without JOINs. Why: the system handles two unrelated sub-domains under the same appeals committee (1xxx building permits and 8xxx/9xxx betterment/197), with different rules and writing style. Without a separation axis, search_similar() and the block-writer's precedent lookup were free to surface betterment-levy paragraphs while drafting a building-permit decision — a real risk of cross-domain contamination. The same axis also lets future domains (national insurance, labor law) coexist without separate schemas. Schema (V4 migration in db.py): - ALTER ... ADD COLUMN IF NOT EXISTS on all five tables + composite indexes (practice_area first). - Idempotent backfill: case_number ~ '^1' → building_permit, '^8' → betterment_levy, '^9' → compensation_197; propagated to documents, chunks, and decisions via case_id; training-corpus rows (case_id NULL) default to appeals_committee. Code: - New services/practice_area.py with derive_subtype, validate, and is_override + enum constants. - db.create_case / create_document / store_chunks / create_decision inherit practice_area from the parent case (or take an explicit override for the case_id=None training corpus). - db.search_similar and search_similar_paragraphs accept practice_area + appeal_subtype filters using the denormalized columns. - tools/search.py auto-resolves the filter from case_number when given. - block_writer._build_precedents_context now passes the active case's practice_area to search_similar_paragraphs — closes the contamination hole for the discussion-block precedent fetch. - tools/cases.case_create auto-derives subtype from case_number; an explicit override that disagrees writes a case_subtype_override entry to audit_log so we can spot bad classifications later. - tools/documents.document_upload_training tags new training material with practice_area + subtype end-to-end (corpus, document, chunks). UI (web/static/index.html + web/app.py): - New-case wizard gets a practice_area dropdown (others disabled until national_insurance / labor_law arrive) and an appeal_subtype dropdown with JS auto-fill from the case-number prefix; manual edits stick. - Case header shows a blue badge with practice_area · subtype. - CaseCreateRequest plumbs both fields through to cases_tools.case_create. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1964,14 +1964,26 @@ kbd {
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>סוג ערר</label>
|
||||
<select id="wiz-committee-type">
|
||||
<option value="רישוי">רישוי ובניה</option>
|
||||
<option value="היטל השבחה">היטל השבחה</option>
|
||||
<option value="פיצויים">פיצויים (ס' 197)</option>
|
||||
<div class="form-group" style="max-width:200px">
|
||||
<label>תחום משפטי</label>
|
||||
<select id="wiz-practice-area">
|
||||
<option value="appeals_committee">ועדת ערר</option>
|
||||
<option value="national_insurance" disabled>ביטוח לאומי (בקרוב)</option>
|
||||
<option value="labor_law" disabled>דיני עבודה (בקרוב)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>סוג ערר <span style="color:#888;font-size:0.85em">(מוסק אוטומטית ממספר התיק)</span></label>
|
||||
<select id="wiz-appeal-subtype">
|
||||
<option value="building_permit">רישוי ובנייה</option>
|
||||
<option value="betterment_levy">היטל השבחה</option>
|
||||
<option value="compensation_197">פיצויים (ס' 197)</option>
|
||||
<option value="unknown">לא ידוע</option>
|
||||
</select>
|
||||
</div>
|
||||
<input type="hidden" id="wiz-committee-type" value="רישוי">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>כתובת נכס</label>
|
||||
<input type="text" id="wiz-address" placeholder="רח' אבינדב 23, קריית יערים">
|
||||
@@ -2730,11 +2742,14 @@ function getListValues(listId) {
|
||||
function buildSummary() {
|
||||
const data = getWizardData();
|
||||
const OUTCOME_LABELS = { rejection: 'דחייה', partial_acceptance: 'קבלה חלקית', full_acceptance: 'קבלה מלאה', betterment_levy: 'היטל השבחה' };
|
||||
const PRACTICE_AREA_LABELS = { appeals_committee: 'ועדת ערר', national_insurance: 'ביטוח לאומי', labor_law: 'דיני עבודה' };
|
||||
const SUBTYPE_LABELS = { building_permit: 'רישוי ובנייה', betterment_levy: 'היטל השבחה', compensation_197: "פיצויים (ס' 197)", unknown: 'לא ידוע' };
|
||||
document.getElementById('wizSummary').innerHTML = `
|
||||
<table style="width:100%;font-size:0.88em;border-collapse:collapse">
|
||||
<tr><td style="padding:6px;color:#888;width:120px">מספר תיק</td><td style="padding:6px;font-weight:600">${esc(data.case_number)}</td></tr>
|
||||
<tr><td style="padding:6px;color:#888">כותרת</td><td style="padding:6px">${esc(data.title)}</td></tr>
|
||||
<tr><td style="padding:6px;color:#888">סוג</td><td style="padding:6px">${esc(data.committee_type)}</td></tr>
|
||||
<tr><td style="padding:6px;color:#888">תחום</td><td style="padding:6px">${esc(PRACTICE_AREA_LABELS[data.practice_area] || data.practice_area)}</td></tr>
|
||||
<tr><td style="padding:6px;color:#888">סוג ערר</td><td style="padding:6px">${esc(SUBTYPE_LABELS[data.appeal_subtype] || data.appeal_subtype)}</td></tr>
|
||||
<tr><td style="padding:6px;color:#888">כתובת</td><td style="padding:6px">${esc(data.property_address || '—')}</td></tr>
|
||||
<tr><td style="padding:6px;color:#888">עוררים</td><td style="padding:6px">${data.appellants.length ? data.appellants.map(esc).join(', ') : '—'}</td></tr>
|
||||
<tr><td style="padding:6px;color:#888">משיבים</td><td style="padding:6px">${data.respondents.length ? data.respondents.map(esc).join(', ') : '—'}</td></tr>
|
||||
@@ -2743,11 +2758,44 @@ function buildSummary() {
|
||||
`;
|
||||
}
|
||||
|
||||
// 1xxx → building_permit, 8xxx → betterment_levy, 9xxx → compensation_197
|
||||
function deriveSubtypeFromCaseNumber(caseNumber) {
|
||||
const m = (caseNumber || '').trim().match(/^(\d)/);
|
||||
if (!m) return 'unknown';
|
||||
return ({1: 'building_permit', 8: 'betterment_levy', 9: 'compensation_197'})[m[1]] || 'unknown';
|
||||
}
|
||||
|
||||
// Auto-fill subtype + committee_type when the user types/edits the case number.
|
||||
// User can override the dropdown manually afterwards.
|
||||
function wireSubtypeAutofill() {
|
||||
const cnInput = document.getElementById('wiz-case-number');
|
||||
const subtypeSel = document.getElementById('wiz-appeal-subtype');
|
||||
const committeeHidden = document.getElementById('wiz-committee-type');
|
||||
if (!cnInput || !subtypeSel) return;
|
||||
const SUBTYPE_TO_COMMITTEE = {
|
||||
building_permit: 'רישוי',
|
||||
betterment_levy: 'היטל השבחה',
|
||||
compensation_197: 'פיצויים',
|
||||
unknown: 'רישוי',
|
||||
};
|
||||
let userOverrode = false;
|
||||
subtypeSel.addEventListener('change', () => { userOverrode = true; });
|
||||
cnInput.addEventListener('input', () => {
|
||||
if (userOverrode) return;
|
||||
const derived = deriveSubtypeFromCaseNumber(cnInput.value);
|
||||
subtypeSel.value = derived;
|
||||
if (committeeHidden) committeeHidden.value = SUBTYPE_TO_COMMITTEE[derived];
|
||||
});
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', wireSubtypeAutofill);
|
||||
|
||||
function getWizardData() {
|
||||
return {
|
||||
case_number: document.getElementById('wiz-case-number').value.trim(),
|
||||
title: document.getElementById('wiz-title').value.trim(),
|
||||
committee_type: document.getElementById('wiz-committee-type').value,
|
||||
practice_area: document.getElementById('wiz-practice-area').value,
|
||||
appeal_subtype: document.getElementById('wiz-appeal-subtype').value,
|
||||
property_address: document.getElementById('wiz-address').value.trim(),
|
||||
permit_number: document.getElementById('wiz-permit').value.trim(),
|
||||
appellants: getListValues('appellantsList'),
|
||||
@@ -2847,9 +2895,18 @@ async function loadCaseView(caseNumber) {
|
||||
new: 'חדש', in_progress: 'בתהליך', documents_ready: 'מסמכים מוכנים',
|
||||
drafted: 'טיוטה', final: 'סופי',
|
||||
};
|
||||
const PRACTICE_AREA_LABELS = { appeals_committee: 'ועדת ערר', national_insurance: 'ביטוח לאומי', labor_law: 'דיני עבודה' };
|
||||
const SUBTYPE_LABELS = { building_permit: 'רישוי ובנייה', betterment_levy: 'היטל השבחה', compensation_197: "פיצויים (ס' 197)", unknown: 'לא ידוע' };
|
||||
const meta = [];
|
||||
meta.push(`<span class="badge ${data.status}">${STATUS_LABELS[data.status] || data.status}</span>`);
|
||||
if (data.committee_type) meta.push(data.committee_type);
|
||||
if (data.practice_area || data.appeal_subtype) {
|
||||
const parts = [];
|
||||
if (data.practice_area) parts.push(PRACTICE_AREA_LABELS[data.practice_area] || data.practice_area);
|
||||
if (data.appeal_subtype) parts.push(SUBTYPE_LABELS[data.appeal_subtype] || data.appeal_subtype);
|
||||
meta.push(`<span class="badge" style="background:#e8f0fe;color:#1a56db" title="תחום משפטי / סוג ערר">${parts.join(' · ')}</span>`);
|
||||
} else if (data.committee_type) {
|
||||
meta.push(data.committee_type);
|
||||
}
|
||||
if (data.property_address) meta.push(data.property_address);
|
||||
if (data.appellants?.length) meta.push('עוררים: ' + data.appellants.join(', '));
|
||||
document.getElementById('caseViewMeta').innerHTML = meta.map(m => `<span>${m}</span>`).join('');
|
||||
|
||||
Reference in New Issue
Block a user