Reorganize: skills/ directory + move memory to docs/

skill-legal-decision/ → skills/decision/
skill-legal-assistant/ → skills/assistant/
skill-legal-docx/ → skills/docx/
memory/*.md → docs/

Also removed: TASKS.md (use TaskMaster), classifier.py (replaced by local_classifier.py)
Updated all references in CLAUDE.md, scripts, PRDs, docs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-04 14:27:07 +00:00
parent d5ccf03e4c
commit 911c797eb2
21 changed files with 224 additions and 316 deletions

View File

@@ -1,248 +0,0 @@
"""סיווג אוטומטי של מסמכים וזיהוי צדדים.
שלוש פונקציות:
1. classify_document — סיווג סוג מסמך (ערר/תשובה/פרוטוקול/...)
2. identify_parties — זיהוי צדדים (עוררים, משיבים, ועדה, מבקשי היתר)
3. detect_appeal_type — זיהוי סוג ערר לפי מספר תיק
"""
from __future__ import annotations
import logging
import re
import anthropic
from legal_mcp import config
from legal_mcp.config import parse_llm_json
logger = logging.getLogger(__name__)
_anthropic_client: anthropic.Anthropic | None = None
def _get_anthropic() -> anthropic.Anthropic:
global _anthropic_client
if _anthropic_client is None:
_anthropic_client = anthropic.Anthropic(api_key=config.ANTHROPIC_API_KEY)
return _anthropic_client
# ── סיווג סוג מסמך ──────────────────────────────────────────────────
DOC_TYPES = {
"appeal": "כתב ערר",
"response": "תשובה / כתב תשובה",
"protocol": "פרוטוקול דיון",
"plan": "תכנית (תב\"ע)",
"permit": "היתר בנייה",
"court_decision": "פסק דין / החלטת בית משפט",
"decision": "החלטת ועדה",
"appraisal": "שומה / חוות דעת שמאית",
"objection": "התנגדות",
"exhibit": "נספח / מסמך תומך",
"reference": "מסמך עזר אחר",
}
CLASSIFY_PROMPT = """אתה מסווג מסמכים משפטיים בתחום תכנון ובניה.
קרא את תחילת המסמך וסווג אותו לאחד מהסוגים הבאים:
- appeal — כתב ערר (מוגש לוועדת ערר)
- response — כתב תשובה (תגובת הצד שכנגד או הוועדה המקומית)
- protocol — פרוטוקול דיון (רישום מדיון שהתקיים)
- plan — תכנית (תב"ע, תכנית מתאר, תכנית מפורטת)
- permit — היתר בנייה (או בקשה להיתר)
- court_decision — פסק דין או החלטה של בית משפט
- decision — החלטת ועדה מקומית או ועדת ערר
- appraisal — שומה או חוות דעת שמאית
- objection — התנגדות (לתכנית, להיתר)
- exhibit — נספח או מסמך תומך
- reference — מסמך עזר אחר
החזר JSON בלבד בפורמט:
{"doc_type": "...", "confidence": 0.0-1.0, "reasoning": "הסבר קצר"}
"""
PARTIES_PROMPT = """אתה מנתח מסמכים משפטיים בתחום תכנון ובניה.
קרא את המסמך וזהה את הצדדים המעורבים. חפש:
- עוררים (appellants) — מי שמגיש את הערר
- משיבים (respondents) — הצד שכנגד (לרוב ועדה מקומית, או מבקש היתר)
- ועדה מקומית (committee) — שם הוועדה המקומית
- מבקשי היתר (permit_applicants) — מי שביקש את ההיתר (אם שונה מהעוררים/משיבים)
החזר JSON בלבד בפורמט:
{
"appellants": ["שם1", "שם2"],
"respondents": ["שם1", "שם2"],
"committee": "שם הוועדה המקומית (אם מצוין)",
"permit_applicants": ["שם1"],
"confidence": 0.0-1.0
}
אם לא ניתן לזהות צד מסוים, החזר רשימה ריקה. אל תמציא שמות.
"""
async def classify_document(text: str) -> dict:
"""סיווג סוג מסמך על בסיס הטקסט.
Args:
text: טקסט המסמך (מספיק 3000 תווים ראשונים)
Returns:
dict עם doc_type, confidence, reasoning
"""
# Use first 3000 chars — usually enough for headers and intro
sample = text[:3000]
client = _get_anthropic()
message = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=512,
messages=[
{
"role": "user",
"content": f"{CLASSIFY_PROMPT}\n\n--- תחילת מסמך ---\n{sample}\n--- סוף דגימה ---",
}
],
)
raw = message.content[0].text.strip()
result = parse_llm_json(raw)
if result is None:
logger.warning("Failed to parse classification response: %s", raw)
return {"doc_type": "reference", "confidence": 0.0, "reasoning": "סיווג נכשל"}
# Validate doc_type
if result.get("doc_type") not in DOC_TYPES:
result["doc_type"] = "reference"
result["confidence"] = 0.0
return result
async def identify_parties(text: str) -> dict:
"""זיהוי צדדים מתוך טקסט מסמך.
Args:
text: טקסט המסמך (מספיק 5000 תווים ראשונים)
Returns:
dict עם appellants, respondents, committee, permit_applicants, confidence
"""
# Use first 5000 chars — parties usually in header/intro
sample = text[:5000]
client = _get_anthropic()
message = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=512,
messages=[
{
"role": "user",
"content": f"{PARTIES_PROMPT}\n\n--- תחילת מסמך ---\n{sample}\n--- סוף דגימה ---",
}
],
)
raw = message.content[0].text.strip()
result = parse_llm_json(raw)
if result is None:
logger.warning("Failed to parse parties response: %s", raw)
return {
"appellants": [],
"respondents": [],
"committee": "",
"permit_applicants": [],
"confidence": 0.0,
}
# Normalize structure
return {
"appellants": result.get("appellants", []),
"respondents": result.get("respondents", []),
"committee": result.get("committee", ""),
"permit_applicants": result.get("permit_applicants", []),
"confidence": result.get("confidence", 0.0),
}
# ── זיהוי סוג ערר לפי מספר תיק ─────────────────────────────────────
APPEAL_TYPES = {
"licensing": "רישוי ובנייה", # 1xxx
"betterment": "היטל השבחה", # 8xxx
"compensation": "פיצויים (ס' 197)", # 9xxx
}
def detect_appeal_type(case_number: str) -> dict:
"""זיהוי סוג ערר לפי מספר תיק.
Convention:
1xxx = רישוי ובנייה
8xxx = היטל השבחה
9xxx = פיצויים (ס' 197)
Args:
case_number: מספר תיק (e.g. "1078-24", "8042-23", "9015-22")
Returns:
dict עם appeal_type, appeal_type_hebrew, confidence
"""
# Extract the numeric prefix before any dash/slash
match = re.match(r"(\d+)", case_number.strip())
if not match:
return {
"appeal_type": "",
"appeal_type_hebrew": "",
"confidence": 0.0,
}
num = int(match.group(1))
first_digit = str(num)[0] if num > 0 else ""
if first_digit == "1":
appeal_type = "licensing"
elif first_digit == "8":
appeal_type = "betterment"
elif first_digit == "9":
appeal_type = "compensation"
else:
return {
"appeal_type": "",
"appeal_type_hebrew": "",
"confidence": 0.5,
}
return {
"appeal_type": appeal_type,
"appeal_type_hebrew": APPEAL_TYPES[appeal_type],
"confidence": 1.0,
}
async def classify_and_identify(text: str, case_number: str = "") -> dict:
"""סיווג מלא: סוג מסמך + צדדים + סוג ערר.
Args:
text: טקסט המסמך
case_number: מספר תיק (אופציונלי, לזיהוי סוג ערר)
Returns:
dict עם classification, parties, appeal_type
"""
classification = await classify_document(text)
parties = await identify_parties(text)
appeal_type = detect_appeal_type(case_number) if case_number else {
"appeal_type": "",
"appeal_type_hebrew": "",
"confidence": 0.0,
}
return {
"classification": classification,
"parties": parties,
"appeal_type": appeal_type,
}