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:
2026-04-11 13:04:30 +00:00
parent 753fe0d57d
commit 0c4886afe6
4 changed files with 192 additions and 3 deletions

View File

@@ -217,6 +217,15 @@ async def draft_section(
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()
async def get_decision_template(case_number: str) -> str:
"""תבנית מבנית להחלטה מלאה עם פרטי התיק."""

View File

@@ -353,3 +353,84 @@ def update_chair_position(
"preview": preview,
"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,
}

View File

@@ -3,9 +3,11 @@
from __future__ import annotations
import json
from pathlib import Path
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 (
CITATION_GUIDANCE,
DECISION_TEMPLATES,
@@ -279,6 +281,32 @@ async def draft_section(
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:
"""קבלת תבנית מבנית להחלטה מלאה עם פרטי התיק, מותאמת לסוג התוצאה הצפויה.