On main: Pre-merge: synced agent files
This commit is contained in:
@@ -204,6 +204,16 @@ async def get_claims(
|
||||
return await documents.get_claims(case_number, party_role)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def document_update_status(
|
||||
case_number: str,
|
||||
doc_title: str,
|
||||
status: str,
|
||||
) -> str:
|
||||
"""עדכון סטטוס עיבוד מסמך. status: pending/extracted/proofread/error."""
|
||||
return await documents.document_update_status(case_number, doc_title, status)
|
||||
|
||||
|
||||
# References
|
||||
@mcp.tool()
|
||||
async def extract_references(
|
||||
@@ -261,6 +271,22 @@ async def draft_section(
|
||||
return await drafting.draft_section(case_number, section, instructions)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def get_research_findings(case_number: str) -> str:
|
||||
"""שליפת ממצאי מחקר — סיכומי פסיקה, מיפוי תכניות, ציר זמן, והמלצות.
|
||||
קורא מ-research-findings.md שנוצר ע"י חוקר התקדימים.
|
||||
"""
|
||||
return await drafting.get_research_findings(case_number)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def get_full_analysis(case_number: str) -> str:
|
||||
"""שליפת הניתוח המשפטי המלא — טענות, תשובות, חוזקות/חולשות, שאלות מחקר,
|
||||
חקיקה, תקדימים ועמדות יו"ר. הכלי המרכזי לכותב לפני כתיבת בלוק י.
|
||||
"""
|
||||
return await drafting.get_full_analysis(case_number)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def get_chair_directions(case_number: str) -> str:
|
||||
"""שליפת עמדות יו"ר הוועדה (דפנה) על סוגיות הערר כ-direction_doc לכותב.
|
||||
@@ -296,6 +322,15 @@ async def save_block_content(
|
||||
return await drafting.save_block_content(case_number, block_id, content)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def get_decision_blocks(
|
||||
case_number: str,
|
||||
block_id: str = "",
|
||||
) -> str:
|
||||
"""שליפת בלוקים שנכתבו — תוכן, מילים, משקלות. ריק = כל הבלוקים."""
|
||||
return await drafting.get_decision_blocks(case_number, block_id)
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def validate_decision(case_number: str) -> str:
|
||||
"""בדיקת QA — 6 בדיקות איכות על ההחלטה. אם בדיקה קריטית נכשלת — ייצוא חסום."""
|
||||
|
||||
@@ -525,14 +525,31 @@ async def _build_precedents_context(case_id: UUID, block_id: str) -> str:
|
||||
text = r["key_quote"] or r["summary"] or ""
|
||||
if text:
|
||||
parts.append(
|
||||
f"[פסיקה: {r['case_number']} {r['case_name']} ({r.get('court', '')})] "
|
||||
f"[פ<EFBFBD><EFBFBD>יקה: {r['case_number']} {r['case_name']} ({r.get('court', '')})] "
|
||||
f"score={r['score']:.3f}\n{text[:400]}"
|
||||
)
|
||||
|
||||
# Search 3: case_precedents (user-attached quotes by Daphna)
|
||||
# These are hand-picked citations — highest priority.
|
||||
attached = await db.list_case_precedents(case_id)
|
||||
if attached:
|
||||
parts.insert(0, "## תקדימים שצורפו ידנית ע\"י יו\"ר הוועדה\n")
|
||||
for i, prec in enumerate(attached):
|
||||
section = prec.get("section_id") or "כללי"
|
||||
citation = prec.get("citation", "")
|
||||
quote = prec.get("quote", "")
|
||||
note = prec.get("chair_note", "")
|
||||
entry = f"[תקדים מצורף #{i+1} — סוגיה: {section}] {citation}"
|
||||
if quote:
|
||||
entry += f"\nציטוט: {quote[:600]}"
|
||||
if note:
|
||||
entry += f"\nהערת יו\"ר: {note}"
|
||||
parts.insert(i + 1, entry)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning("Failed to fetch precedents: %s", e)
|
||||
|
||||
return "\n\n".join(parts) if parts else "(אין תקדימים)"
|
||||
return "\n\n".join(parts) if parts else "(אין תקד<EFBFBD><EFBFBD>מים)"
|
||||
|
||||
|
||||
async def _build_style_context() -> str:
|
||||
|
||||
@@ -26,6 +26,13 @@ CHAIR_POSITION_PLACEHOLDERS = (
|
||||
"[טרם מולא]",
|
||||
)
|
||||
|
||||
# Any text starting with these prefixes is also a placeholder
|
||||
# (the analyst sometimes adds explanatory text after the bracket)
|
||||
CHAIR_POSITION_PLACEHOLDER_PREFIXES = (
|
||||
"[ימולא",
|
||||
"ימולא ע",
|
||||
)
|
||||
|
||||
CHAIR_POSITION_LABEL = "עמדת ועדת הערר"
|
||||
|
||||
# Matches "## N. title" or "## title" for main sections
|
||||
@@ -47,6 +54,9 @@ CASE_NUMBER_RE = re.compile(r"#\s*ניתוח.*?ערר\s+([\d/\-]+)", re.MULTILIN
|
||||
DATE_RE = re.compile(r"^תאריך:\s*(.+?)\s*$", re.MULTILINE)
|
||||
|
||||
|
||||
RESEARCH_FINDINGS_FILENAME = "research-findings.md"
|
||||
|
||||
|
||||
def _is_placeholder(text: str) -> bool:
|
||||
"""Check if a field value is one of the placeholder strings (empty)."""
|
||||
stripped = text.strip()
|
||||
@@ -55,6 +65,9 @@ def _is_placeholder(text: str) -> bool:
|
||||
for ph in CHAIR_POSITION_PLACEHOLDERS:
|
||||
if ph in stripped:
|
||||
return True
|
||||
for prefix in CHAIR_POSITION_PLACEHOLDER_PREFIXES:
|
||||
if stripped.startswith(prefix):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
@@ -434,3 +447,199 @@ def extract_chair_directions(file_path: Path) -> dict[str, Any]:
|
||||
"threshold_claims": threshold,
|
||||
"issues": issues,
|
||||
}
|
||||
|
||||
|
||||
# ── Full analysis extraction (for legal-writer) ──────────────────
|
||||
|
||||
|
||||
# Map Hebrew field labels → stable English keys for JSON output
|
||||
_FIELD_KEY_MAP = {
|
||||
"טענה": "claims",
|
||||
"טענה (claim)": "claims",
|
||||
"טענות": "claims",
|
||||
"תשובה": "responses",
|
||||
"תשובה (response)": "responses",
|
||||
"תשובות": "responses",
|
||||
"תגובה": "replies",
|
||||
"תגובה (reply)": "replies",
|
||||
"תגובות": "replies",
|
||||
# Analyst sometimes appends party name to the label
|
||||
# e.g. "תגובה (reply — קובר)" — catch the pattern dynamically below
|
||||
"ניתוח אסטרטגי": "strategic_analysis",
|
||||
"חוזקות": "strengths",
|
||||
"חולשות": "weaknesses",
|
||||
"הזדמנויות": "opportunities",
|
||||
"שאלות משפטיות": "legal_questions",
|
||||
"חיפוש תקדימים": "precedent_search",
|
||||
"חקיקה רלוונטית": "relevant_legislation",
|
||||
"תקדימים מהקורפוס הפנימי": "internal_precedents",
|
||||
}
|
||||
|
||||
|
||||
def _fields_to_dict(fields: list[dict]) -> dict[str, str]:
|
||||
"""Convert ordered field list to a dict with stable English keys.
|
||||
|
||||
Unknown labels are kept as-is (Hebrew) so no data is lost.
|
||||
Handles dynamic labels like "תגובה (reply — קובר)" by matching prefix.
|
||||
"""
|
||||
result: dict[str, str] = {}
|
||||
for f in fields:
|
||||
label = f["label"]
|
||||
key = _FIELD_KEY_MAP.get(label)
|
||||
if key is None:
|
||||
# Try prefix matching for dynamic labels (e.g. "תגובה (reply — name)")
|
||||
if label.startswith("תגובה"):
|
||||
key = "replies"
|
||||
elif label.startswith("טענה"):
|
||||
key = "claims"
|
||||
elif label.startswith("תשובה"):
|
||||
key = "responses"
|
||||
else:
|
||||
key = label
|
||||
result[key] = f["content"]
|
||||
return result
|
||||
|
||||
|
||||
def extract_full_analysis(file_path: Path) -> dict[str, Any]:
|
||||
"""Extract the complete strategic analysis from analysis-and-research.md.
|
||||
|
||||
Unlike extract_chair_directions (which returns only chair positions),
|
||||
this returns ALL fields per issue: claims, responses, replies,
|
||||
strengths/weaknesses/opportunities, legal questions, legislation,
|
||||
and internal precedents — everything the legal-writer needs to
|
||||
produce block-yod (discussion).
|
||||
|
||||
Returns the same envelope as extract_chair_directions (status, counts)
|
||||
plus full field data in each item.
|
||||
"""
|
||||
if not file_path.exists():
|
||||
return {
|
||||
"file_exists": False,
|
||||
"status": "missing",
|
||||
"error": "analysis-and-research.md not found",
|
||||
"procedural_background": "",
|
||||
"agreed_facts": "",
|
||||
"disputed_facts": "",
|
||||
"conclusions": "",
|
||||
"threshold_claims": [],
|
||||
"issues": [],
|
||||
"total_items": 0,
|
||||
"filled_count": 0,
|
||||
"empty_count": 0,
|
||||
}
|
||||
|
||||
parsed = parse(file_path)
|
||||
|
||||
def enrich_item(item: dict) -> dict:
|
||||
"""Return full item with all fields as a flat dict."""
|
||||
enriched = {
|
||||
"id": item["id"],
|
||||
"number": item["number"],
|
||||
"title": item["title"],
|
||||
"direction": item.get("chair_position", "") or "",
|
||||
}
|
||||
# Add all extracted fields with stable keys
|
||||
enriched.update(_fields_to_dict(item.get("fields", [])))
|
||||
return enriched
|
||||
|
||||
threshold = [enrich_item(t) for t in parsed.get("threshold_claims", [])]
|
||||
issues = [enrich_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", ""),
|
||||
"modified_at": parsed.get("header", {}).get("modified_at", ""),
|
||||
"status": status,
|
||||
"total_items": total,
|
||||
"filled_count": filled,
|
||||
"empty_count": empty,
|
||||
"procedural_background": parsed.get("procedural_background", ""),
|
||||
"agreed_facts": parsed.get("agreed_facts", ""),
|
||||
"disputed_facts": parsed.get("disputed_facts", ""),
|
||||
"conclusions": parsed.get("conclusions", ""),
|
||||
"threshold_claims": threshold,
|
||||
"issues": issues,
|
||||
}
|
||||
|
||||
|
||||
# ── Research findings extraction ──────────────────────────────────
|
||||
|
||||
|
||||
def extract_research_findings(file_path: Path) -> dict[str, Any]:
|
||||
"""Extract structured research findings from research-findings.md.
|
||||
|
||||
The file is produced by the legal-researcher agent and contains:
|
||||
precedent summaries, plan mappings, timeline, and recommendations.
|
||||
Returns a structured dict or a status-only dict if file is missing.
|
||||
"""
|
||||
if not file_path.exists():
|
||||
return {
|
||||
"file_exists": False,
|
||||
"status": "missing",
|
||||
"error": "research-findings.md not found",
|
||||
}
|
||||
|
||||
content = file_path.read_text(encoding="utf-8")
|
||||
stat = file_path.stat()
|
||||
mtime_iso = datetime.fromtimestamp(stat.st_mtime).isoformat()
|
||||
|
||||
sections = _split_main_sections(content)
|
||||
|
||||
result: dict[str, Any] = {
|
||||
"file_exists": True,
|
||||
"file_path": str(file_path),
|
||||
"modified_at": mtime_iso,
|
||||
"file_size": stat.st_size,
|
||||
"precedent_summaries": [],
|
||||
"plan_mappings": [],
|
||||
"timeline": "",
|
||||
"recommendations": "",
|
||||
"other_sections": [],
|
||||
}
|
||||
|
||||
for _number, title, body in sections:
|
||||
title_norm = title.strip()
|
||||
if "סיכום פסיקה" in title_norm or "פסיקה" in title_norm:
|
||||
subs = _split_subsections(body)
|
||||
for sub_title, sub_body in subs:
|
||||
fields = _extract_fields(sub_body)
|
||||
result["precedent_summaries"].append({
|
||||
"title": sub_title,
|
||||
"fields": {f["label"]: f["content"] for f in fields},
|
||||
"raw": sub_body if not fields else "",
|
||||
})
|
||||
elif "מיפוי תכנית" in title_norm or "תכנית" in title_norm:
|
||||
subs = _split_subsections(body)
|
||||
for sub_title, sub_body in subs:
|
||||
fields = _extract_fields(sub_body)
|
||||
result["plan_mappings"].append({
|
||||
"title": sub_title,
|
||||
"fields": {f["label"]: f["content"] for f in fields},
|
||||
"raw": sub_body if not fields else "",
|
||||
})
|
||||
elif "ציר זמן" in title_norm:
|
||||
result["timeline"] = body
|
||||
elif "המלצות" in title_norm:
|
||||
result["recommendations"] = body
|
||||
else:
|
||||
result["other_sections"].append({
|
||||
"title": title_norm,
|
||||
"body": body,
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
@@ -383,3 +383,49 @@ async def get_claims(case_number: str, party_role: str = "") -> str:
|
||||
})
|
||||
|
||||
return json.dumps(formatted, default=str, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
async def document_update_status(
|
||||
case_number: str,
|
||||
doc_title: str,
|
||||
status: str,
|
||||
) -> str:
|
||||
"""עדכון סטטוס עיבוד מסמך (extraction_status).
|
||||
|
||||
ערכים אפשריים: pending, extracted, proofread, error.
|
||||
|
||||
Args:
|
||||
case_number: מספר תיק הערר
|
||||
doc_title: שם/כותרת המסמך (או חלק ממנו)
|
||||
status: הסטטוס החדש
|
||||
"""
|
||||
valid_statuses = ("pending", "extracted", "proofread", "error")
|
||||
if status not in valid_statuses:
|
||||
return f"סטטוס לא חוקי: {status}. ערכים אפשריים: {', '.join(valid_statuses)}"
|
||||
|
||||
case = await db.get_case_by_number(case_number)
|
||||
if not case:
|
||||
return f"תיק {case_number} לא נמצא."
|
||||
|
||||
case_id = UUID(case["id"])
|
||||
docs = await db.get_documents(case_id)
|
||||
|
||||
# Find matching document by title (partial match)
|
||||
matched = None
|
||||
for d in docs:
|
||||
if doc_title.lower() in d.get("title", "").lower():
|
||||
matched = d
|
||||
break
|
||||
|
||||
if not matched:
|
||||
titles = [d.get("title", "?") for d in docs]
|
||||
return f"מסמך '{doc_title}' לא נמצא בתיק {case_number}. מסמכים קיימים: {titles}"
|
||||
|
||||
doc_id = UUID(matched["id"])
|
||||
await db.update_document(doc_id, extraction_status=status)
|
||||
|
||||
return json.dumps({
|
||||
"updated": True,
|
||||
"document": matched["title"],
|
||||
"new_status": status,
|
||||
}, ensure_ascii=False, indent=2)
|
||||
|
||||
@@ -281,6 +281,39 @@ async def draft_section(
|
||||
return json.dumps(context, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
async def get_research_findings(case_number: str) -> str:
|
||||
"""שליפת ממצאי מחקר — סיכומי פסיקה, מיפוי תכניות, ציר זמן, והמלצות.
|
||||
|
||||
קורא מ-research-findings.md שנוצר ע"י סוכן חוקר התקדימים (legal-researcher).
|
||||
מחזיר JSON מובנה עם הממצאים, או status=missing אם הקובץ לא קיים עדיין.
|
||||
|
||||
Args:
|
||||
case_number: מספר תיק הערר
|
||||
"""
|
||||
case_dir = config.find_case_dir(case_number)
|
||||
file_path = case_dir / "documents" / "research" / research_md.RESEARCH_FINDINGS_FILENAME
|
||||
result = research_md.extract_research_findings(file_path)
|
||||
return json.dumps(result, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
async def get_full_analysis(case_number: str) -> str:
|
||||
"""שליפת הניתוח המשפטי המלא מ-analysis-and-research.md — כולל טענות, תשובות,
|
||||
תגובות, ניתוח אסטרטגי (חוזקות/חולשות/הזדמנויות), שאלות מחקר, חקיקה רלוונטית,
|
||||
תקדימים פנימיים, ועמדות יו"ר הוועדה.
|
||||
|
||||
זה הכלי המרכזי שכותב ההחלטה צריך לקרוא **לפני** כתיבת בלוק י (דיון).
|
||||
מחזיר JSON מובנה עם כל השדות שהניתוח המשפטי הפיק, ולא רק עמדות כמו
|
||||
get_chair_directions.
|
||||
|
||||
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_full_analysis(file_path)
|
||||
return json.dumps(result, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
async def get_chair_directions(case_number: str) -> str:
|
||||
"""שליפת עמדות יו"ר הוועדה (דפנה) על סוגיות הערר, לצורך יצירת direction_doc
|
||||
לכותב. קורא מ-analysis-and-research.md (שנוצר ע"י legal-analyst ומולא ע"י
|
||||
@@ -454,6 +487,71 @@ async def save_block_content(case_number: str, block_id: str, content: str) -> s
|
||||
return str(e)
|
||||
|
||||
|
||||
async def get_decision_blocks(case_number: str, block_id: str = "") -> str:
|
||||
"""שליפת בלוקים שנכתבו בהחלטה — תוכן, ספירת מילים, משקלות.
|
||||
|
||||
אם block_id ריק — מחזיר את כל הבלוקים. אם מצוין — רק בלוק ספציפי.
|
||||
שימושי לבודק איכות (QA) שצריך לקרוא בלוקים בודדים.
|
||||
|
||||
Args:
|
||||
case_number: מספר תיק הערר
|
||||
block_id: מזהה בלוק (ריק = כולם). למשל: block-yod, block-vav
|
||||
"""
|
||||
case = await db.get_case_by_number(case_number)
|
||||
if not case:
|
||||
return f"תיק {case_number} לא נמצא."
|
||||
|
||||
case_id = UUID(case["id"])
|
||||
decision = await db.get_decision_by_case(case_id)
|
||||
if not decision:
|
||||
return f"אין החלטה בתיק {case_number}."
|
||||
|
||||
decision_id = UUID(decision["id"])
|
||||
pool = await db.get_pool()
|
||||
async with pool.acquire() as conn:
|
||||
if block_id:
|
||||
rows = await conn.fetch(
|
||||
"SELECT block_id, block_index, title, content, word_count, "
|
||||
"weight_percent, status FROM decision_blocks "
|
||||
"WHERE decision_id = $1 AND block_id = $2 "
|
||||
"ORDER BY block_index",
|
||||
decision_id, block_id,
|
||||
)
|
||||
else:
|
||||
rows = await conn.fetch(
|
||||
"SELECT block_id, block_index, title, content, word_count, "
|
||||
"weight_percent, status FROM decision_blocks "
|
||||
"WHERE decision_id = $1 ORDER BY block_index",
|
||||
decision_id,
|
||||
)
|
||||
|
||||
if not rows:
|
||||
if block_id:
|
||||
return f"בלוק {block_id} לא נמצא בהחלטה."
|
||||
return "אין בלוקים בהחלטה."
|
||||
|
||||
blocks = []
|
||||
total_words = 0
|
||||
for r in rows:
|
||||
total_words += r["word_count"] or 0
|
||||
blocks.append({
|
||||
"block_id": r["block_id"],
|
||||
"index": r["block_index"],
|
||||
"title": r["title"],
|
||||
"content": r["content"],
|
||||
"word_count": r["word_count"],
|
||||
"weight_percent": float(r["weight_percent"] or 0),
|
||||
"status": r["status"],
|
||||
})
|
||||
|
||||
return json.dumps({
|
||||
"case_number": case_number,
|
||||
"total_blocks": len(blocks),
|
||||
"total_words": total_words,
|
||||
"blocks": blocks,
|
||||
}, default=str, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
async def analyze_style() -> str:
|
||||
"""הרצת ניתוח סגנון על קורפוס ההחלטות של דפנה. מחלץ דפוסי כתיבה ושומר אותם."""
|
||||
from legal_mcp.services.style_analyzer import analyze_corpus
|
||||
|
||||
Reference in New Issue
Block a user