"""MCP tools for the internal-decisions citation graph (TaskMaster #34). The citation graph captures pointers between Daphna's (and other internal committee chairs') decisions: when one ruling cites another, ``precedent_ internal_citations`` records the edge — resolved against ``case_law`` when the cited row exists, kept as a stub when it doesn't. Three tools: - ``extract_internal_citations`` — run regex extraction on one row (by id) or on every internal-committee row filtered by chair (e.g. Daphna only). Idempotent: re-running does not duplicate rows (ON CONFLICT DO NOTHING). - ``list_internal_citations`` — outgoing edges from a source row. Optional ``linked_only`` filter for rows resolved to existing case_law UUIDs. - ``list_incoming_citations`` — incoming edges to a target row ("which Daphna decisions cite this ruling?"). These tools are *manual triggers*. The pipeline runs them after a new internal-decision upload, but the chair / researcher can also re-run on demand (for example after fixing OCR or after uploading a previously- missing decision so that newer rows now link to it). """ from __future__ import annotations import json from uuid import UUID from legal_mcp.services import citation_extractor def _ok(payload) -> str: return json.dumps(payload, ensure_ascii=False, indent=2, default=str) def _err(msg: str) -> str: return json.dumps({"error": msg}, ensure_ascii=False) async def extract_internal_citations( case_law_id: str = "", chair_name: str = "", limit: int = 0, ) -> str: """חילוץ ציטוטים פנימיים מהחלטות ועדת ערר ושמירה ב-precedent_internal_citations. Args: case_law_id: UUID של החלטה ספציפית. אם ריק וגם chair_name ריק — מריץ על כל ההחלטות internal_committee. אם מסופק, חייב לעבור על שורה אחת בלבד (משתמש בזה אחרי upload). chair_name: שם יו"ר (כגון 'דפנה תמיר'). מסנן את האצווה. ריק = כל היו"רים. limit: עליון על מספר רשומות שיעובדו (0 = ללא הגבלה). שימושי לבדיקה. הכלי איידמפוטנטי — ON CONFLICT DO NOTHING על (source_case_law_id, cited_case_number). מחזיר סטטיסטיקה: extracted, linked, new, skipped, failed. """ if case_law_id.strip() and chair_name.strip(): return _err("יש לספק case_law_id או chair_name, לא שניהם") if case_law_id.strip(): try: cl_uuid = UUID(case_law_id.strip()) except ValueError: return _err("case_law_id לא תקין") try: stats = await citation_extractor.extract_and_store(cl_uuid) except Exception as e: return _err(str(e)) return _ok(stats) try: stats = await citation_extractor.extract_all_internal_committee( chair_name_filter=chair_name.strip(), limit=int(limit) if limit else 0, ) except Exception as e: return _err(str(e)) return _ok(stats) async def list_internal_citations( case_law_id: str = "", linked_only: bool = False, limit: int = 50, ) -> str: """רשימת ציטוטים יוצאים מהחלטה (מה ההחלטה הזו מצטטת). Args: case_law_id: UUID של ה-case_law (חובה). linked_only: True = רק ציטוטים שקושרו ל-case_law קיים בקורפוס. limit: עליון על מספר תוצאות (default 50). Returns: JSON עם list של ציטוטים, כולל target_case_number/name/chair כשהם linked. אם linked_only=False, ציטוטים בלתי קושרים יחזרו עם cited_case_law_id=null וניתן להעלות אותם דרך internal_decision_upload. """ if not case_law_id.strip(): return _err("case_law_id חובה") try: cl_uuid = UUID(case_law_id.strip()) except ValueError: return _err("case_law_id לא תקין") try: rows = await citation_extractor.list_citations_for_case_law( cl_uuid, linked_only=bool(linked_only), ) except Exception as e: return _err(str(e)) return _ok({"items": rows[: max(1, int(limit))], "count": len(rows)}) async def list_incoming_citations( case_law_id: str = "", limit: int = 50, ) -> str: """רשימת ציטוטים נכנסים אל החלטה (אילו החלטות מצטטות אותה). שימוש: רוצים לדעת אילו החלטות של דפנה הסתמכו על פסק דין מסוים? מעבירים את ה-case_law_id של פסק הדין הזה. Args: case_law_id: UUID של ה-target case_law (חובה). limit: עליון על מספר תוצאות. """ if not case_law_id.strip(): return _err("case_law_id חובה") try: cl_uuid = UUID(case_law_id.strip()) except ValueError: return _err("case_law_id לא תקין") try: rows = await citation_extractor.list_citations_to_case_law(cl_uuid) except Exception as e: return _err(str(e)) return _ok({"items": rows[: max(1, int(limit))], "count": len(rows)})