Add content checklists for block-yod and chair feedback system
Addresses Dafna's observation that licensing decisions lack comprehensive planning discussion. Systematic corpus analysis of all 24 training decisions revealed the system learned writing style but not substantive content. Changes: - Corpus analysis of all 24 decisions (docs/corpus-analysis.md) - 5 content checklists by appeal subtype injected into block-yod prompt - chair_feedback DB table + API endpoints + MCP tools - Feedback management page in Next.js UI (/feedback) - Navigation updated with "הערות יו״ר" link Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -346,6 +346,29 @@ async def ingest_final_version(
|
||||
return await workflow.ingest_final_version(case_number, file_path, final_text)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def record_chair_feedback(
|
||||
case_number: str,
|
||||
feedback_text: str,
|
||||
block_id: str = "block-yod",
|
||||
category: str = "missing_content",
|
||||
lesson_extracted: str = "",
|
||||
) -> str:
|
||||
"""תיעוד הערת יו"ר (דפנה) על טיוטת החלטה — חסר, שגיאה, סגנון."""
|
||||
return await workflow.record_chair_feedback(
|
||||
case_number, feedback_text, block_id, category, lesson_extracted,
|
||||
)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def list_chair_feedback(
|
||||
case_number: str = "",
|
||||
category: str = "",
|
||||
unresolved_only: bool = True,
|
||||
) -> str:
|
||||
"""הצגת הערות יו"ר שתועדו — אפשר לסנן לפי תיק, קטגוריה, מטופלות."""
|
||||
return await workflow.list_chair_feedback(case_number, category, unresolved_only)
|
||||
|
||||
|
||||
def main():
|
||||
mcp.run(transport="stdio")
|
||||
|
||||
@@ -20,6 +20,7 @@ from uuid import UUID
|
||||
|
||||
from legal_mcp import config
|
||||
from legal_mcp.services import db, embeddings, claude_session
|
||||
from legal_mcp.services.lessons import get_content_checklist
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -215,6 +216,8 @@ BLOCK_PROMPTS = {
|
||||
- **ללא כותרות משנה** (חריג: נושאים נפרדים לחלוטין)
|
||||
- מספור רציף
|
||||
|
||||
{content_checklist}
|
||||
|
||||
## כיוון מאושר (חובה):
|
||||
{direction_context}
|
||||
|
||||
@@ -310,6 +313,15 @@ async def write_block(
|
||||
outcome = (decision or {}).get("outcome", "rejected")
|
||||
structure_guidance = STRUCTURE_GUIDANCE.get(outcome, "")
|
||||
|
||||
# Content checklist — tells block-yod WHAT topics to cover
|
||||
content_checklist = ""
|
||||
if block_id == "block-yod":
|
||||
content_checklist = get_content_checklist(
|
||||
appeal_type=case.get("appeal_type", ""),
|
||||
subject=case.get("subject", ""),
|
||||
subject_categories=case.get("subject_categories", []),
|
||||
)
|
||||
|
||||
# Format prompt — per Anthropic long-context best practices:
|
||||
# Place source documents FIRST (top of prompt), instructions LAST.
|
||||
# "Queries at the end can improve response quality by up to 30%"
|
||||
@@ -323,6 +335,7 @@ async def write_block(
|
||||
style_context=style_context,
|
||||
discussion_context=discussion_context,
|
||||
structure_guidance=structure_guidance,
|
||||
content_checklist=content_checklist,
|
||||
)
|
||||
|
||||
# Restructure: sources first, then instructions
|
||||
|
||||
@@ -358,6 +358,22 @@ CREATE TABLE IF NOT EXISTS case_law_embeddings (
|
||||
created_at TIMESTAMPTZ DEFAULT now()
|
||||
);
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════
|
||||
-- Chair Feedback (הערות דפנה על טיוטות)
|
||||
-- ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
CREATE TABLE IF NOT EXISTS chair_feedback (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
case_id UUID REFERENCES cases(id) ON DELETE SET NULL,
|
||||
block_id TEXT DEFAULT '', -- block-yod, block-vav, etc.
|
||||
feedback_text TEXT NOT NULL, -- ההערה של דפנה
|
||||
category TEXT DEFAULT 'other', -- missing_content/wrong_tone/wrong_structure/factual_error/style/other
|
||||
lesson_extracted TEXT DEFAULT '', -- הלקח שהופק
|
||||
applied_to TEXT[] DEFAULT '{}', -- לאילו קבצים/כללים הלקח יושם
|
||||
resolved BOOLEAN DEFAULT FALSE, -- האם הלקח יושם
|
||||
created_at TIMESTAMPTZ DEFAULT now()
|
||||
);
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════
|
||||
-- Indexes
|
||||
-- ═══════════════════════════════════════════════════════════════════
|
||||
@@ -986,3 +1002,72 @@ async def search_precedents(
|
||||
|
||||
results.sort(key=lambda x: x["score"], reverse=True)
|
||||
return results[:limit]
|
||||
|
||||
|
||||
# ── Chair feedback ────────────────────────────────────────────────
|
||||
|
||||
async def record_chair_feedback(
|
||||
case_id: UUID | None,
|
||||
block_id: str,
|
||||
feedback_text: str,
|
||||
category: str = "other",
|
||||
lesson_extracted: str = "",
|
||||
) -> UUID:
|
||||
"""Record feedback from the chair (Dafna) on a draft block."""
|
||||
pool = await get_pool()
|
||||
feedback_id = uuid4()
|
||||
async with pool.acquire() as conn:
|
||||
await conn.execute(
|
||||
"""INSERT INTO chair_feedback
|
||||
(id, case_id, block_id, feedback_text, category, lesson_extracted)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)""",
|
||||
feedback_id, case_id, block_id, feedback_text, category,
|
||||
lesson_extracted,
|
||||
)
|
||||
return feedback_id
|
||||
|
||||
|
||||
async def list_chair_feedback(
|
||||
case_id: UUID | None = None,
|
||||
category: str | None = None,
|
||||
unresolved_only: bool = False,
|
||||
) -> list[dict]:
|
||||
"""List chair feedback, optionally filtered."""
|
||||
pool = await get_pool()
|
||||
conditions = []
|
||||
params: list = []
|
||||
idx = 1
|
||||
|
||||
if case_id:
|
||||
conditions.append(f"case_id = ${idx}")
|
||||
params.append(case_id)
|
||||
idx += 1
|
||||
if category:
|
||||
conditions.append(f"category = ${idx}")
|
||||
params.append(category)
|
||||
idx += 1
|
||||
if unresolved_only:
|
||||
conditions.append("resolved = FALSE")
|
||||
|
||||
where = f"WHERE {' AND '.join(conditions)}" if conditions else ""
|
||||
async with pool.acquire() as conn:
|
||||
rows = await conn.fetch(
|
||||
f"SELECT * FROM chair_feedback {where} ORDER BY created_at DESC",
|
||||
*params,
|
||||
)
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
|
||||
async def resolve_chair_feedback(
|
||||
feedback_id: UUID,
|
||||
applied_to: list[str],
|
||||
) -> None:
|
||||
"""Mark feedback as resolved and record where it was applied."""
|
||||
pool = await get_pool()
|
||||
async with pool.acquire() as conn:
|
||||
await conn.execute(
|
||||
"""UPDATE chair_feedback
|
||||
SET resolved = TRUE, applied_to = $2
|
||||
WHERE id = $1""",
|
||||
feedback_id, applied_to,
|
||||
)
|
||||
|
||||
@@ -329,3 +329,193 @@ def format_ratios_comment(outcome: str, section: str) -> str:
|
||||
lo, hi = ratios[section]
|
||||
return f"יעד: {lo}-{hi}% מסך ההחלטה"
|
||||
return ""
|
||||
|
||||
|
||||
# ── Content checklists by appeal subtype ──────────────────────────
|
||||
# Based on systematic analysis of 24 decisions from Dafna's corpus.
|
||||
# See: docs/corpus-analysis.md
|
||||
|
||||
CONTENT_CHECKLISTS: dict[str, str] = {
|
||||
"licensing_substantive": """## צ'קליסט תוכן — ערר רישוי מהותי (חובה)
|
||||
הדיון חייב לכלול את הנושאים הרלוונטיים מהרשימה הבאה.
|
||||
**אל תדלג על נושא שרלוונטי לתיק — בדוק כל סעיף.**
|
||||
|
||||
### א. הקשר תכנוני רחב (חובה בכל ערר מהותי)
|
||||
- תכניות חלות — ציין את התכניות הרלוונטיות ברמה מקומית, מחוזית וארצית (לפי הצורך)
|
||||
- ייעוד הקרקע — מה הייעוד בתכנית? מה השימושים המותרים?
|
||||
- אופי הסביבה — מרקם בנוי, צפיפות, אופי שכונה/ישוב
|
||||
- *דוגמה*: בערר פרומר — 12 סעיפים על MI/200, תמ"א 35, תמ"מ 30/1
|
||||
|
||||
### ב. ניתוח הוראות תכנית (כשיש שאלה של התאמה/סטייה)
|
||||
- ציטוט ישיר מהוראות התכנית הרלוונטיות (200-600 מילים לכל ציטוט)
|
||||
- פרשנות — מה תכלית ההוראה?
|
||||
- יישום — האם הבקשה תואמת או סוטה?
|
||||
- *דוגמה*: בערר לבנון — ניתוח חתכים של נספח בינוי מול הבקשה
|
||||
|
||||
### ג. חניה (כשרלוונטי — מופיע ב-8 מתוך 24 החלטות)
|
||||
- הוראות תכנית + נספח תנועה (ציטוט ישיר)
|
||||
- חישוב מקומות חניה נדרשים vs. מסופקים
|
||||
- חלופות: קרן חניה, חפיפת שימושים, קרבה לתח"צ
|
||||
- *דוגמה*: בערר בית הכרם — 8 סעיפים, 400+ מילים מהוראות תכנית 5166ב
|
||||
|
||||
### ד. קווי בניין ומרווחים (כשרלוונטי)
|
||||
- הוראת תכנית על מרווחים
|
||||
- סטייה ניכרת? — תקנה 2(19) / הלכת בן-יקר-גת
|
||||
- הצדקה + מידתיות — פגיעה בשכנים?
|
||||
|
||||
### ה. גובה וקומות (כשרלוונטי)
|
||||
- הוראת תכנית + נספח בינוי (חתכים)
|
||||
- מטרת ההגבלה — למה יש הגבלת גובה כאן?
|
||||
- סטייה ניכרת — תקנה 2(10) / 2(8)
|
||||
|
||||
### ו. פגיעה בשכנים (כשרלוונטי)
|
||||
- ממצאי סיור באתר
|
||||
- השפעה: צל, פרטיות, רעש, נוף
|
||||
- מידתיות — האם הפגיעה סבירה?
|
||||
|
||||
### ז. שימוש חורג (כשרלוונטי)
|
||||
- מה השימוש המותר בתכנית? מה השימוש המבוקש?
|
||||
- "מבחן ההתאמה" — האם השימוש מתאים למיקום?
|
||||
- תנאים ומגבלות
|
||||
""",
|
||||
|
||||
"licensing_threshold": """## צ'קליסט תוכן — ערר רישוי סף/סמכות
|
||||
הערר עוסק בשאלות סף — אין צורך בדיון תכנוני מקיף.
|
||||
|
||||
### א. שאלת הסמכות
|
||||
- סעיפי חוק רלוונטיים (ס' 12ב, 152, וכו')
|
||||
- פסיקה על גבולות הסמכות
|
||||
|
||||
### ב. זכות ערר
|
||||
- מי רשאי לערור? באיזה מסלול?
|
||||
- הלכת שפר (עע"מ 317/10) — כשרלוונטית
|
||||
|
||||
### ג. שיהוי (אם רלוונטי)
|
||||
""",
|
||||
|
||||
"licensing_property": """## צ'קליסט תוכן — ערר רישוי קנייני
|
||||
הערר עוסק בעיקר בשאלת תימוכין קנייניים — דיון משפטי.
|
||||
|
||||
### א. מסגרת נורמטיבית
|
||||
- הלכת עייזן, בני אליעזר, רוזן — "היתכנות קניינית"
|
||||
- ס' 71ב לחוק המקרקעין
|
||||
|
||||
### ב. בחינת הראיות
|
||||
- הסכמות, רישום, היסטוריית בנייה
|
||||
- חלוקה דה-פקטו ארוכת שנים
|
||||
|
||||
### ג. הפרדה בין קניין לתכנון
|
||||
- גוף תכנוני אינו מכריע בסכסוכי קניין
|
||||
- "היתכנות קניינית" ≠ הוכחת בעלות
|
||||
|
||||
### ד. שאלות תכנוניות (אם רלוונטיות)
|
||||
- אם הערר עולה גם שאלות תכנוניות — דון בהן בנפרד
|
||||
""",
|
||||
|
||||
"tama38": """## צ'קליסט תוכן — ערר תמ"א 38
|
||||
הדיון חייב לאזן בין אינטרס ציבורי לפגיעה בשכנים.
|
||||
|
||||
### א. אינטרס ציבורי — חיזוק/התחדשות
|
||||
- עוצמת האינטרס — בניין גדול vs. בית בודד
|
||||
- "בית בודד" מחליש את אינטרס החיזוק
|
||||
- תרומה לרקמה העירונית
|
||||
|
||||
### ב. תכנית אב / מדיניות אזורית
|
||||
- האם יש תכנית אב? מדיניות 16000?
|
||||
- התאמה לראיה כללית vs. אד-הוק
|
||||
|
||||
### ג. ניתוח השוואתי
|
||||
- זכויות לפי תכנית קיימת vs. מבוקש לפי תמ"א 38
|
||||
- שטחים, קומות, קווי בניין — טבלת השוואה
|
||||
|
||||
### ד. שימור (כשרלוונטי)
|
||||
- חוות דעת אגף שימור
|
||||
- השפעה על מיקום/צורת הבניין
|
||||
|
||||
### ה. חניה (כמעט תמיד רלוונטי)
|
||||
- הוראות תכנית + ס' 17 לתמ"א 38
|
||||
- פטורים — קרבה לתח"צ, קרן חניה, תכנית אב
|
||||
- ניתוח מפורט של חלופות
|
||||
|
||||
### ו. פגיעה בשכנים
|
||||
- ממצאי סיור
|
||||
- צל, פרטיות, קרבה
|
||||
- מידתיות — מה הפגיעה ביחס לתועלת?
|
||||
|
||||
### ז. מטרדי בנייה
|
||||
- "מטרד בנייה אינו עילה לסירוב" — אך תנאים נדרשים
|
||||
- תכנית ארגון אתר
|
||||
""",
|
||||
|
||||
"betterment_levy": """## צ'קליסט תוכן — ערר היטל השבחה
|
||||
⚠️ שים לב: אין עדיין החלטות היטל השבחה בקורפוס האימון.
|
||||
הצ'קליסט הזה מבוסס על ידע כללי — לא על ניתוח ספציפי של סגנון דפנה.
|
||||
|
||||
### א. המסגרת הנורמטיבית
|
||||
- התוספת השלישית לחוק התכנון והבנייה
|
||||
- אירוע מס — מה יצר את ההשבחה?
|
||||
|
||||
### ב. שומה
|
||||
- שיטת השומה (שומה מכרעת / שמאי מייעץ)
|
||||
- מועד הקובע
|
||||
- זכויות בנייה — לפני ואחרי
|
||||
|
||||
### ג. שאלות משפטיות
|
||||
- פטורים (ס' 19)
|
||||
- מועדי תשלום
|
||||
- שיערוך
|
||||
|
||||
### ד. ניתוח שמאי
|
||||
- האם השומה תקינה?
|
||||
- פערים בין השומות
|
||||
""",
|
||||
}
|
||||
|
||||
|
||||
def get_content_checklist(
|
||||
appeal_type: str = "",
|
||||
subject: str = "",
|
||||
subject_categories: list[str] | None = None,
|
||||
) -> str:
|
||||
"""Return the appropriate content checklist based on case characteristics.
|
||||
|
||||
Determines the subtype from case metadata:
|
||||
- TAMA 38 cases → tama38 checklist
|
||||
- Betterment levy (8xxx) → betterment_levy checklist
|
||||
- Property-only cases → licensing_property checklist
|
||||
- Threshold/jurisdiction cases → licensing_threshold checklist
|
||||
- All other licensing → licensing_substantive checklist
|
||||
"""
|
||||
cats = subject_categories or []
|
||||
subject_lower = subject.lower() if subject else ""
|
||||
appeal_lower = appeal_type.lower() if appeal_type else ""
|
||||
|
||||
# TAMA 38
|
||||
if any(
|
||||
kw in subject_lower
|
||||
for kw in ["תמ\"א 38", "תמא 38", "תמ\"א38", "חיזוק", "tama"]
|
||||
) or "תמ\"א 38" in cats:
|
||||
return CONTENT_CHECKLISTS["tama38"]
|
||||
|
||||
# Betterment levy
|
||||
if "היטל השבחה" in appeal_lower or "betterment" in appeal_lower or any(
|
||||
"היטל" in c for c in cats
|
||||
):
|
||||
return CONTENT_CHECKLISTS["betterment_levy"]
|
||||
|
||||
# Property-focused (תימוכין קנייניים)
|
||||
if any(
|
||||
kw in subject_lower
|
||||
for kw in ["תימוכין", "קנייני", "בעלות", "הסכמת דיירים"]
|
||||
):
|
||||
return CONTENT_CHECKLISTS["licensing_property"]
|
||||
|
||||
# Threshold/jurisdiction
|
||||
if any(
|
||||
kw in subject_lower
|
||||
for kw in ["סמכות", "סף", "סילוק על הסף", "זכות ערר"]
|
||||
):
|
||||
return CONTENT_CHECKLISTS["licensing_threshold"]
|
||||
|
||||
# Default: substantive licensing
|
||||
return CONTENT_CHECKLISTS["licensing_substantive"]
|
||||
|
||||
@@ -318,3 +318,97 @@ async def ingest_final_version(
|
||||
return json.dumps(result, default=str, ensure_ascii=False, indent=2)
|
||||
except ValueError as e:
|
||||
return json.dumps({"status": "error", "message": str(e)}, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
# ── Chair feedback tools ──────────────────────────────────────────
|
||||
|
||||
|
||||
async def record_chair_feedback(
|
||||
case_number: str,
|
||||
feedback_text: str,
|
||||
block_id: str = "block-yod",
|
||||
category: str = "missing_content",
|
||||
lesson_extracted: str = "",
|
||||
) -> str:
|
||||
"""תיעוד הערת יו"ר (דפנה) על טיוטת החלטה.
|
||||
|
||||
Args:
|
||||
case_number: מספר תיק הערר
|
||||
feedback_text: ההערה של דפנה (מה חסר, מה לא נכון, מה צריך לשנות)
|
||||
block_id: הבלוק שההערה מתייחסת אליו (ברירת מחדל: block-yod)
|
||||
category: קטגוריה — missing_content/wrong_tone/wrong_structure/factual_error/style/other
|
||||
lesson_extracted: הלקח שהופק מההערה (אם ברור כבר)
|
||||
"""
|
||||
case = await db.get_case_by_number(case_number)
|
||||
case_id = UUID(case["id"]) if case else None
|
||||
|
||||
valid_categories = [
|
||||
"missing_content", "wrong_tone", "wrong_structure",
|
||||
"factual_error", "style", "other",
|
||||
]
|
||||
if category not in valid_categories:
|
||||
return f"קטגוריה לא חוקית. אפשרויות: {', '.join(valid_categories)}"
|
||||
|
||||
feedback_id = await db.record_chair_feedback(
|
||||
case_id=case_id,
|
||||
block_id=block_id,
|
||||
feedback_text=feedback_text,
|
||||
category=category,
|
||||
lesson_extracted=lesson_extracted,
|
||||
)
|
||||
|
||||
return json.dumps({
|
||||
"status": "ok",
|
||||
"feedback_id": str(feedback_id),
|
||||
"message": f"הערה נרשמה בהצלחה. קטגוריה: {category}.",
|
||||
"next_steps": [
|
||||
"כדי להפיק לקח מההערה, הפעל: analyze_chair_feedback",
|
||||
"כדי לסמן כמטופל: resolve_chair_feedback",
|
||||
],
|
||||
}, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
async def list_chair_feedback(
|
||||
case_number: str = "",
|
||||
category: str = "",
|
||||
unresolved_only: bool = True,
|
||||
) -> str:
|
||||
"""הצגת הערות יו"ר שתועדו, עם אפשרות סינון.
|
||||
|
||||
Args:
|
||||
case_number: סינון לפי תיק (אם ריק — כל ההערות)
|
||||
category: סינון לפי קטגוריה
|
||||
unresolved_only: האם להציג רק הערות שלא טופלו (ברירת מחדל: כן)
|
||||
"""
|
||||
case_id = None
|
||||
if case_number:
|
||||
case = await db.get_case_by_number(case_number)
|
||||
if case:
|
||||
case_id = UUID(case["id"])
|
||||
|
||||
feedbacks = await db.list_chair_feedback(
|
||||
case_id=case_id,
|
||||
category=category or None,
|
||||
unresolved_only=unresolved_only,
|
||||
)
|
||||
|
||||
if not feedbacks:
|
||||
return "אין הערות שמתאימות לסינון."
|
||||
|
||||
items = []
|
||||
for fb in feedbacks:
|
||||
items.append({
|
||||
"id": str(fb["id"]),
|
||||
"case_id": str(fb["case_id"]) if fb["case_id"] else None,
|
||||
"block_id": fb["block_id"],
|
||||
"category": fb["category"],
|
||||
"feedback": fb["feedback_text"],
|
||||
"lesson": fb["lesson_extracted"],
|
||||
"resolved": fb["resolved"],
|
||||
"date": fb["created_at"].isoformat() if fb.get("created_at") else None,
|
||||
})
|
||||
|
||||
return json.dumps({
|
||||
"total": len(items),
|
||||
"feedbacks": items,
|
||||
}, ensure_ascii=False, indent=2, default=str)
|
||||
|
||||
Reference in New Issue
Block a user