Add outcome-aware drafting, lessons system, and improved style analysis

- Add expected_outcome field to cases (rejection/partial/full/betterment_levy)
- New lessons.py module with golden ratios, templates, and drafting guidance per outcome type
- Style analyzer now uses Opus with full decision text (no truncation), with multi-pass fallback for large corpora
- Drafting tool provides outcome-specific templates, section guidance, and ratio comments
- Improved JSON extraction with bracket-matching fallback

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-24 18:58:42 +00:00
parent 6f515dc2cb
commit 39089dcef5
6 changed files with 726 additions and 71 deletions

View File

@@ -22,6 +22,7 @@ async def case_create(
committee_type: str = "ועדה מקומית",
hearing_date: str = "",
notes: str = "",
expected_outcome: str = "",
) -> str:
"""יצירת תיק ערר חדש.
@@ -36,6 +37,7 @@ async def case_create(
committee_type: סוג הוועדה (ברירת מחדל: ועדה מקומית)
hearing_date: תאריך דיון (YYYY-MM-DD)
notes: הערות
expected_outcome: תוצאה צפויה (rejection/partial_acceptance/full_acceptance/betterment_levy)
"""
from datetime import date as date_type
@@ -54,6 +56,7 @@ async def case_create(
committee_type=committee_type,
hearing_date=h_date,
notes=notes,
expected_outcome=expected_outcome,
)
# Initialize git repo for the case
@@ -122,6 +125,7 @@ async def case_update(
hearing_date: str = "",
decision_date: str = "",
tags: list[str] | None = None,
expected_outcome: str = "",
) -> str:
"""עדכון פרטי תיק.
@@ -134,6 +138,7 @@ async def case_update(
hearing_date: תאריך דיון (YYYY-MM-DD)
decision_date: תאריך החלטה (YYYY-MM-DD)
tags: תגיות
expected_outcome: תוצאה צפויה (rejection/partial_acceptance/full_acceptance/betterment_levy)
"""
from datetime import date as date_type
@@ -156,6 +161,8 @@ async def case_update(
fields["decision_date"] = date_type.fromisoformat(decision_date)
if tags is not None:
fields["tags"] = tags
if expected_outcome:
fields["expected_outcome"] = expected_outcome
updated = await db.update_case(UUID(case["id"]), **fields)

View File

@@ -6,8 +6,21 @@ import json
from uuid import UUID
from legal_mcp.services import db, embeddings
from legal_mcp.services.lessons import (
CITATION_GUIDANCE,
DECISION_TEMPLATES,
DISCUSSION_RULES,
GOLDEN_RATIOS,
OPENING_STRATEGIES,
PARAGRAPH_LENGTHS,
SUMMARY_STRATEGIES,
TRANSITION_PHRASES,
VALID_OUTCOMES,
format_ratios_comment,
get_lessons_for_outcome,
)
# Fallback template for cases without expected_outcome
DECISION_TEMPLATE = """# החלטה
## בפני: דפנה תמיר, יו"ר ועדת הערר מחוז ירושלים
@@ -51,38 +64,106 @@ DECISION_TEMPLATE = """# החלטה
async def get_style_guide() -> str:
"""שליפת דפוסי הסגנון של דפנה - נוסחאות, ביטויים אופייניים ומבנה."""
"""שליפת דפוסי הסגנון של דפנה - נוסחאות, ביטויים אופייניים ומבנה, כולל לקחים מעשיים."""
patterns = await db.get_style_patterns()
if not patterns:
return "לא נמצאו דפוסי סגנון. יש להעלות החלטות קודמות ולהריץ ניתוח סגנון (/style-report)."
grouped: dict[str, list] = {}
for p in patterns:
pt = p["pattern_type"]
if pt not in grouped:
grouped[pt] = []
grouped[pt].append({
"text": p["pattern_text"],
"context": p["context"],
"frequency": p["frequency"],
})
type_names = {
"opening_formula": "נוסחאות פתיחה",
"transition": "ביטויי מעבר",
"citation_style": "סגנון ציטוט",
"analysis_structure": "מבנה ניתוח",
"closing_formula": "נוסחאות סיום",
"characteristic_phrase": "ביטויים אופייניים",
}
result = "# מדריך סגנון - דפנה תמיר\n\n"
for ptype, items in grouped.items():
result += f"## {type_names.get(ptype, ptype)}\n\n"
for item in items:
result += f"- **{item['text']}** ({item['context']}, תדירות: {item['frequency']})\n"
result += "\n"
# Part 1: DB-sourced patterns (from analyze_style)
if patterns:
type_names = {
"opening_formula": "נוסחאות פתיחה",
"transition": "ביטויי מעבר",
"citation_style": "סגנון ציטוט",
"analysis_structure": "מבנה ניתוח",
"closing_formula": "נוסחאות סיום",
"characteristic_phrase": "ביטויים אופייניים",
"argument_flow": "זרימת טיעון",
"evidence_handling": "התייחסות לראיות",
}
grouped: dict[str, list] = {}
for p in patterns:
pt = p["pattern_type"]
if pt not in grouped:
grouped[pt] = []
grouped[pt].append({
"text": p["pattern_text"],
"context": p["context"],
"frequency": p["frequency"],
})
for ptype, items in grouped.items():
result += f"## {type_names.get(ptype, ptype)}\n\n"
for item in items:
result += f"- **{item['text']}** ({item['context']}, תדירות: {item['frequency']})\n"
result += "\n"
else:
result += "_לא נמצאו דפוסים מקורפוס. יש להעלות החלטות ולהריץ /style-report._\n\n"
# Part 2: Lessons-based guidance
result += "---\n\n# לקחים מעשיים\n\n"
# Universal discussion rules
result += "## כללי דיון אוניברסליים\n\n"
for rule in DISCUSSION_RULES["universal"]:
result += f"- {rule}\n"
result += "\n"
# Citation technique
result += "## טכניקת ציטוט\n\n"
result += f"{CITATION_GUIDANCE}\n\n"
# Transition phrases
result += "## ביטויי מעבר (מהשוואת טיוטות)\n\n"
for p in TRANSITION_PHRASES:
ctx = p["context"]
outcome_note = f" [בעיקר ב-{p['outcome']}]" if p["outcome"] else ""
result += f"- **{p['phrase']}** — {ctx}{outcome_note}\n"
result += "\n"
# Paragraph lengths
result += "## אורכי פסקאות מומלצים\n\n"
result += "| סוג | מילים |\n|-----|------|\n"
labels = {
"claims": "טענות",
"discussion_regular": "דיון רגיל",
"discussion_with_citation": "דיון + ציטוט",
"discussion_average": "ממוצע דיון",
}
for key, (lo, hi) in PARAGRAPH_LENGTHS.items():
result += f"| {labels.get(key, key)} | {lo}-{hi} |\n"
result += "\n"
# Golden ratios
result += "## יחסי הזהב (אחוזי סעיפים מסך ההחלטה)\n\n"
result += "| סוג ערר | רקע | טענות | דיון | סיכום |\n"
result += "|---------|------|-------|------|-------|\n"
outcome_labels = {
"rejection": "רישוי נדחה",
"full_acceptance": "רישוי מתקבל",
"partial_acceptance": "רישוי קבלה חלקית",
"betterment_levy": "היטל השבחה",
}
for outcome in VALID_OUTCOMES:
r = GOLDEN_RATIOS[outcome]
result += (
f"| {outcome_labels[outcome]} "
f"| {r['background'][0]}-{r['background'][1]}% "
f"| {r['claims'][0]}-{r['claims'][1]}% "
f"| {r['discussion'][0]}-{r['discussion'][1]}% "
f"| {r['summary'][0]}-{r['summary'][1]}% |\n"
)
result += "\n"
# Opening and summary strategies
result += "## אסטרטגיות פתיחה וסיכום לפי תוצאה\n\n"
for outcome in VALID_OUTCOMES:
opening = OPENING_STRATEGIES[outcome]
summary = SUMMARY_STRATEGIES[outcome]
result += f"### {outcome_labels[outcome]}\n"
result += f"- **פתיחה:** {opening['description']} ({opening['paragraphs'][0]}-{opening['paragraphs'][1]} פסקאות)\n"
result += f"- **סיכום ({summary['heading']}):** {summary['description']}\n\n"
return result
@@ -104,6 +185,7 @@ async def draft_section(
return f"תיק {case_number} לא נמצא."
case_id = UUID(case["id"])
expected_outcome = case.get("expected_outcome", "")
# 1. Get relevant chunks from case documents
section_query = {
@@ -139,6 +221,7 @@ async def draft_section(
"respondents": case["respondents"],
"subject": case["subject"],
"property_address": case["property_address"],
"expected_outcome": expected_outcome,
},
"section": section,
"instructions": instructions,
@@ -167,11 +250,37 @@ async def draft_section(
],
}
# 4. Add outcome-aware drafting guidance
if expected_outcome and expected_outcome in VALID_OUTCOMES:
lessons = get_lessons_for_outcome(expected_outcome)
guidance: dict = {
"outcome": expected_outcome,
"golden_ratios": lessons["golden_ratios"],
"citation_guidance": lessons["citation_guidance"],
}
# Section-specific guidance
if section == "legal_analysis":
guidance["discussion_rules"] = lessons["discussion_rules"]
guidance["opening_strategy"] = lessons["opening_strategy"]
guidance["transition_phrases"] = lessons["transition_phrases"]
guidance["paragraph_lengths"] = lessons["paragraph_lengths"]
elif section in ("conclusion", "ruling"):
guidance["summary_strategy"] = lessons["summary_strategy"]
elif section == "facts":
guidance["target_ratio"] = lessons["golden_ratios"].get("background", "")
guidance["paragraph_lengths"] = {"claims": lessons["paragraph_lengths"].get("claims", "")}
elif section in ("appellant_claims", "respondent_claims"):
guidance["target_ratio"] = lessons["golden_ratios"].get("claims", "")
guidance["paragraph_lengths"] = {"claims": lessons["paragraph_lengths"].get("claims", "")}
context["drafting_guidance"] = guidance
return json.dumps(context, ensure_ascii=False, indent=2)
async def get_decision_template(case_number: str) -> str:
"""קבלת תבנית מבנית להחלטה מלאה עם פרטי התיק.
"""קבלת תבנית מבנית להחלטה מלאה עם פרטי התיק, מותאמת לסוג התוצאה הצפויה.
Args:
case_number: מספר תיק הערר
@@ -182,7 +291,9 @@ async def get_decision_template(case_number: str) -> str:
if not case:
return f"תיק {case_number} לא נמצא."
template = DECISION_TEMPLATE.format(
expected_outcome = case.get("expected_outcome", "")
format_args = dict(
case_number=case["case_number"],
subject=case["subject"],
appellants=", ".join(case.get("appellants", [])),
@@ -191,7 +302,34 @@ async def get_decision_template(case_number: str) -> str:
date=date.today().strftime("%d.%m.%Y"),
)
return template
# Use outcome-specific template if available
if expected_outcome and expected_outcome in DECISION_TEMPLATES:
# Add ratio comments
format_args["ratios_background"] = format_ratios_comment(expected_outcome, "background")
format_args["ratios_claims"] = format_ratios_comment(expected_outcome, "claims")
format_args["ratios_discussion"] = format_ratios_comment(expected_outcome, "discussion")
format_args["ratios_summary"] = format_ratios_comment(expected_outcome, "summary")
template = DECISION_TEMPLATES[expected_outcome].format(**format_args)
# Add guidance header
opening = OPENING_STRATEGIES[expected_outcome]
summary = SUMMARY_STRATEGIES[expected_outcome]
header = (
f"<!-- תבנית מותאמת ל: {expected_outcome} -->\n"
f"<!-- פתיחת דיון: {opening['description']} -->\n"
f"<!-- סיכום: {summary['description']} -->\n\n"
)
return header + template
else:
# Fallback to generic template
template = DECISION_TEMPLATE.format(**format_args)
if not expected_outcome:
template = (
"<!-- לא הוגדרה תוצאה צפויה. הגדר expected_outcome בתיק לקבלת תבנית מותאמת. -->\n"
f"<!-- ערכים אפשריים: {', '.join(VALID_OUTCOMES)} -->\n\n"
) + template
return template
async def analyze_style() -> str: