Wire legal-writer to chair directions from analysis-and-research.md
Closes the loop so דפנה's positions (written inline in the UI and
saved to analysis-and-research.md) automatically become binding
direction for the legal-writer agent — no manual copy-paste,
no bypass.
Backend:
- research_md.extract_chair_directions(path) returns a compact dict
with status (missing/empty/partial/complete), filled_count,
empty_count, and a reduced list of threshold_claims + issues each
with {id, number, title, direction}. Designed to be directly usable
as direction_doc by the writer.
- New MCP tool: drafting.get_chair_directions(case_number) wraps the
helper, resolves the case research file path via config.find_case_dir,
returns formatted JSON.
- Registered in server.py as mcp__legal-ai__get_chair_directions.
legal-writer agent update:
- Adds get_chair_directions to the tools list.
- New mandatory "שלב 1ב" before any block writing: call
get_chair_directions, branch on status.
- missing → halt, report "legal-analyst לא רץ עדיין"
- empty → halt, instruct Dafna to fill positions via the UI URL
- partial → halt unless user confirms; write only filled sections
- complete → proceed
- New "שלב 1ג" constructs an internal direction_doc from the
received chair rulings before writing block י.
- Block י section expanded with 5 binding rules:
1. Open each discussion with Dafna's ruling as the thesis
2. Frame the reasoning in her style (use get_style_guide phrases)
3. Match her tone (decisive vs nuanced)
4. Must NOT contradict her position — if she disagreed with your
own inclination, her position rules
5. Use legal_questions from the analysis file as the analytical
structure (principle question first, concrete application second)
- New bullet section for block יא: summarize each chair ruling
briefly, state final outcome, close with the signed date formula.
Verified all four status paths (missing/empty/partial/complete) via
local test. Now Dafna's workflow is fully end-to-end: she reads the
analyst report in the UI, fills "עמדת ועדת הערר" in each card, hits
blur to auto-save, then triggers legal-writer — which picks up her
positions as direction without any file shuffle.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,7 @@ tools:
|
|||||||
- mcp__legal-ai__document_list
|
- mcp__legal-ai__document_list
|
||||||
- mcp__legal-ai__document_get_text
|
- mcp__legal-ai__document_get_text
|
||||||
- mcp__legal-ai__get_claims
|
- mcp__legal-ai__get_claims
|
||||||
|
- mcp__legal-ai__get_chair_directions
|
||||||
- mcp__legal-ai__get_decision_template
|
- mcp__legal-ai__get_decision_template
|
||||||
- mcp__legal-ai__get_block_context
|
- mcp__legal-ai__get_block_context
|
||||||
- mcp__legal-ai__save_block_content
|
- mcp__legal-ai__save_block_content
|
||||||
@@ -71,8 +72,53 @@ tools:
|
|||||||
### שלב 1: הכנה
|
### שלב 1: הכנה
|
||||||
1. קרא פרטי התיק (`case_get`)
|
1. קרא פרטי התיק (`case_get`)
|
||||||
2. קרא טענות מחולצות (`get_claims`)
|
2. קרא טענות מחולצות (`get_claims`)
|
||||||
3. קבל תבנית החלטה (`get_decision_template`)
|
3. **קרא את עמדות יו"ר הוועדה (`get_chair_directions`) — חובה!**
|
||||||
4. קרא מדריך סגנון (`get_style_guide`)
|
4. קבל תבנית החלטה (`get_decision_template`)
|
||||||
|
5. קרא מדריך סגנון (`get_style_guide`)
|
||||||
|
|
||||||
|
### שלב 1ב: בדיקת עמדות יו"ר — חובה לפני כתיבה!
|
||||||
|
|
||||||
|
ה-`get_chair_directions` מחזיר status:
|
||||||
|
|
||||||
|
- **`missing`** — הקובץ `analysis-and-research.md` לא קיים.
|
||||||
|
⛔ **עצור מייד.** הסוכן `legal-analyst` לא רץ עדיין על התיק.
|
||||||
|
דווח ל-Paperclip: "לא ניתן לכתוב טיוטה — ניתוח משפטי טרם בוצע.
|
||||||
|
יש להריץ את legal-analyst קודם."
|
||||||
|
|
||||||
|
- **`empty`** — הקובץ קיים אבל דפנה לא מילאה אף עמדה.
|
||||||
|
⛔ **עצור מייד.** דווח ל-Paperclip: "לא ניתן לכתוב טיוטה —
|
||||||
|
כל X הסוגיות ממתינות לעמדת יו"ר הוועדה. יש להיכנס לדף התיק
|
||||||
|
ב-UI (https://legal-ai.nautilus.marcusgroup.org/#/case/{case_number})
|
||||||
|
ולמלא את השדה 'עמדת ועדת הערר' בכל סוגיה."
|
||||||
|
|
||||||
|
- **`partial`** — חלק מהסוגיות מולאו, אחרות ריקות.
|
||||||
|
⚠️ **עצור.** דווח למשתמשת שחסרות Y מתוך X עמדות. **רק**
|
||||||
|
אם המשתמשת מאשרת מפורשות להמשיך (למשל, כי היא רוצה טיוטה
|
||||||
|
חלקית), אפשר להמשיך — ולכתוב רק עבור הסוגיות שמולאו, ולציין
|
||||||
|
ב-comment את הסוגיות שלא טופלו.
|
||||||
|
|
||||||
|
- **`complete`** — כל העמדות מולאו. ✅ **ניתן להמשיך.**
|
||||||
|
|
||||||
|
### שלב 1ג: בניית direction_doc מעמדות היו"ר
|
||||||
|
|
||||||
|
לפני כתיבת בלוק י (דיון), בנה direction_doc פנימי מהעמדות שקיבלת:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"threshold_claims": [
|
||||||
|
{"id": "threshold_1", "title": "...", "chair_ruling": "..."},
|
||||||
|
...
|
||||||
|
],
|
||||||
|
"issues": [
|
||||||
|
{"id": "issue_1", "title": "...", "chair_ruling": "..."},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
כל `chair_ruling` הוא הטקסט הגולמי שדפנה כתבה. הוא **מחייב אותך** —
|
||||||
|
אסור לך לסתור את דעתה של דפנה, רק לנסח אותה בצורה משפטית מקצועית
|
||||||
|
בסגנון שלה.
|
||||||
|
|
||||||
### שלב 2: כתיבה בלוק-אחרי-בלוק
|
### שלב 2: כתיבה בלוק-אחרי-בלוק
|
||||||
לכל בלוק (ה עד יא):
|
לכל בלוק (ה עד יא):
|
||||||
@@ -104,3 +150,28 @@ case_update(case_number, status="drafted")
|
|||||||
- השתמש בציטוטים ארוכים (200-600 מילים) מפסיקה
|
- השתמש בציטוטים ארוכים (200-600 מילים) מפסיקה
|
||||||
- אל תחזור על עובדות מבלוק ו
|
- אל תחזור על עובדות מבלוק ו
|
||||||
- אל תשתמש בכותרות משנה (למעט נושאים נפרדים לחלוטין)
|
- אל תשתמש בכותרות משנה (למעט נושאים נפרדים לחלוטין)
|
||||||
|
|
||||||
|
### חובה: שימוש בעמדות יו"ר מ-`get_chair_directions`
|
||||||
|
|
||||||
|
עבור **כל טענת סף** ו**כל סוגיה** ב-direction_doc שבנית בשלב 1ג:
|
||||||
|
|
||||||
|
1. **פתח את הדיון במסקנה של דפנה** — למשל "**טענת הסף הראשונה נדחית**"
|
||||||
|
או "**בסוגיה זו אנו מקבלים את עמדת העוררים**", **על בסיס** מה
|
||||||
|
שדפנה כתבה ב-`chair_ruling`.
|
||||||
|
2. **נסח את הנימוק** בסגנון דפנה — השתמש בביטויי מעבר מ-`get_style_guide`
|
||||||
|
("נחדד", "ודוק", "יחד עם זאת", "מכאן כי"), פסיקה שמוזכרת
|
||||||
|
ב-`internal_precedents` של הסוגיה, וחקיקה מ-`relevant_legislation`.
|
||||||
|
3. **עקוב אחר הטון של דפנה** — אם היא כתבה "יש לדחות זאת מכל וכל"
|
||||||
|
אל תנסח מתון ("ייתכן שהוועדה תמצא לנכון..."). אם היא כתבה
|
||||||
|
"נראה לי שיש מקום לקבל בחלקה" אל תנסח חד ("הערר מתקבל במלואו").
|
||||||
|
4. **אסור לסתור את דעתה של דפנה.** אם היא כתבה דעה שמנוגדת לעמדתך —
|
||||||
|
דעתה קובעת. אתה מנסח את הטיעון המשפטי בעד **עמדתה**.
|
||||||
|
5. **ציון שאלות המחקר** — בכל סוגיה, השתמש ב-`legal_questions`
|
||||||
|
שחולצו ב-analysis-and-research.md כמבנה לניתוח (שאלה עקרונית
|
||||||
|
תחילה, ואז יישום קונקרטי).
|
||||||
|
|
||||||
|
## בלוק יא — סיכום
|
||||||
|
|
||||||
|
- חזור על המסקנות של דפנה מה-`chair_ruling` של כל סוגיה בקצרה
|
||||||
|
- ציין את התוצאה הסופית (ערר מתקבל/נדחה/מתקבל בחלקו) בהתאם לעמדות
|
||||||
|
- הוסף את פסקת "ניתנה פה אחד" עם תאריך עברי ולועזי
|
||||||
|
|||||||
@@ -217,6 +217,15 @@ async def draft_section(
|
|||||||
return await drafting.draft_section(case_number, section, instructions)
|
return await drafting.draft_section(case_number, section, instructions)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def get_chair_directions(case_number: str) -> str:
|
||||||
|
"""שליפת עמדות יו"ר הוועדה (דפנה) על סוגיות הערר כ-direction_doc לכותב.
|
||||||
|
קורא מ-analysis-and-research.md שמולא ע"י דפנה דרך ה-UI.
|
||||||
|
מחזיר סטטוס (missing/empty/partial/complete) + עמדות מובנות.
|
||||||
|
"""
|
||||||
|
return await drafting.get_chair_directions(case_number)
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def get_decision_template(case_number: str) -> str:
|
async def get_decision_template(case_number: str) -> str:
|
||||||
"""תבנית מבנית להחלטה מלאה עם פרטי התיק."""
|
"""תבנית מבנית להחלטה מלאה עם פרטי התיק."""
|
||||||
|
|||||||
@@ -353,3 +353,84 @@ def update_chair_position(
|
|||||||
"preview": preview,
|
"preview": preview,
|
||||||
"timestamp": datetime.now().isoformat(),
|
"timestamp": datetime.now().isoformat(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ── Chair directions extraction (for downstream agents) ─────────
|
||||||
|
|
||||||
|
|
||||||
|
def extract_chair_directions(file_path: Path) -> dict[str, Any]:
|
||||||
|
"""Extract only the chair positions from analysis-and-research.md.
|
||||||
|
|
||||||
|
Returns a compact dict that the legal-writer agent can use as direction:
|
||||||
|
|
||||||
|
{
|
||||||
|
"case_number": "1033-25",
|
||||||
|
"file_path": "...",
|
||||||
|
"file_exists": True,
|
||||||
|
"total_items": 9,
|
||||||
|
"filled_count": 3,
|
||||||
|
"empty_count": 6,
|
||||||
|
"status": "partial", # "empty" | "partial" | "complete"
|
||||||
|
"threshold_claims": [
|
||||||
|
{"id": "threshold_1", "number": 1, "title": "...", "direction": "..."},
|
||||||
|
...
|
||||||
|
],
|
||||||
|
"issues": [
|
||||||
|
{"id": "issue_1", "number": 1, "title": "...", "direction": "..."},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
Used by legal-writer to convert chair positions into direction docs
|
||||||
|
before generating blocks of the decision.
|
||||||
|
"""
|
||||||
|
if not file_path.exists():
|
||||||
|
return {
|
||||||
|
"file_exists": False,
|
||||||
|
"status": "missing",
|
||||||
|
"error": "analysis-and-research.md not found",
|
||||||
|
"threshold_claims": [],
|
||||||
|
"issues": [],
|
||||||
|
"total_items": 0,
|
||||||
|
"filled_count": 0,
|
||||||
|
"empty_count": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed = parse(file_path)
|
||||||
|
|
||||||
|
def reduce_item(item: dict) -> dict:
|
||||||
|
return {
|
||||||
|
"id": item["id"],
|
||||||
|
"number": item["number"],
|
||||||
|
"title": item["title"],
|
||||||
|
"direction": item.get("chair_position", "") or "",
|
||||||
|
}
|
||||||
|
|
||||||
|
threshold = [reduce_item(t) for t in parsed.get("threshold_claims", [])]
|
||||||
|
issues = [reduce_item(i) for i in parsed.get("issues", [])]
|
||||||
|
|
||||||
|
all_items = threshold + issues
|
||||||
|
total = len(all_items)
|
||||||
|
filled = sum(1 for x in all_items if x["direction"].strip())
|
||||||
|
empty = total - filled
|
||||||
|
|
||||||
|
if total == 0:
|
||||||
|
status = "missing"
|
||||||
|
elif filled == 0:
|
||||||
|
status = "empty"
|
||||||
|
elif filled == total:
|
||||||
|
status = "complete"
|
||||||
|
else:
|
||||||
|
status = "partial"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"file_exists": True,
|
||||||
|
"file_path": str(file_path),
|
||||||
|
"case_number": parsed.get("header", {}).get("case_number", ""),
|
||||||
|
"status": status,
|
||||||
|
"total_items": total,
|
||||||
|
"filled_count": filled,
|
||||||
|
"empty_count": empty,
|
||||||
|
"threshold_claims": threshold,
|
||||||
|
"issues": issues,
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,9 +3,11 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
from pathlib import Path
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from legal_mcp.services import db, embeddings
|
from legal_mcp import config
|
||||||
|
from legal_mcp.services import db, embeddings, research_md
|
||||||
from legal_mcp.services.lessons import (
|
from legal_mcp.services.lessons import (
|
||||||
CITATION_GUIDANCE,
|
CITATION_GUIDANCE,
|
||||||
DECISION_TEMPLATES,
|
DECISION_TEMPLATES,
|
||||||
@@ -279,6 +281,32 @@ async def draft_section(
|
|||||||
return json.dumps(context, ensure_ascii=False, indent=2)
|
return json.dumps(context, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_chair_directions(case_number: str) -> str:
|
||||||
|
"""שליפת עמדות יו"ר הוועדה (דפנה) על סוגיות הערר, לצורך יצירת direction_doc
|
||||||
|
לכותב. קורא מ-analysis-and-research.md (שנוצר ע"י legal-analyst ומולא ע"י
|
||||||
|
דפנה דרך ה-UI).
|
||||||
|
|
||||||
|
מחזיר JSON עם סטטוס, כמה סוגיות מולאו וכמה עדיין ריקות, ורשימה של עמדות
|
||||||
|
מובנות — ניתן להזריק ישירות כ-direction_doc לבלוק י (דיון) ולבלוק יא (סיכום).
|
||||||
|
|
||||||
|
סטטוסים:
|
||||||
|
missing — הקובץ לא קיים
|
||||||
|
empty — הקובץ קיים אבל כל העמדות ריקות (טרם נקבעה דעה)
|
||||||
|
partial — חלק מהעמדות מולאו
|
||||||
|
complete — כל העמדות מולאו
|
||||||
|
|
||||||
|
אם המצב הוא `empty` או `missing` — הכותב צריך לעצור ולבקש מדפנה למלא
|
||||||
|
את הקובץ דרך ה-UI לפני המשך הכתיבה.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
case_number: מספר תיק הערר
|
||||||
|
"""
|
||||||
|
case_dir = config.find_case_dir(case_number)
|
||||||
|
file_path = case_dir / "documents" / "research" / "analysis-and-research.md"
|
||||||
|
result = research_md.extract_chair_directions(file_path)
|
||||||
|
return json.dumps(result, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
|
||||||
async def get_decision_template(case_number: str) -> str:
|
async def get_decision_template(case_number: str) -> str:
|
||||||
"""קבלת תבנית מבנית להחלטה מלאה עם פרטי התיק, מותאמת לסוג התוצאה הצפויה.
|
"""קבלת תבנית מבנית להחלטה מלאה עם פרטי התיק, מותאמת לסוג התוצאה הצפויה.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user