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:
@@ -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,
|
||||
}
|
||||
Reference in New Issue
Block a user