All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 5s
כל קריאות text→JSON ב-9 המחלצים העבירו את ברירת-המחדל של ה-CLI (כל הכלים פעילים). המודל פלט מדי פעם stop_reason:"tool_use", מה שמפיל את --max-turns 1 ל-error_max_turns ומאלץ retry — ~$0.12-0.16 לניסיון, × 3. נצפה ב-drain חילוץ-ההלכות (legal-halacha-drain, 15 כשלי error_max_turns ב-error.log). התשתית כבר קיימת: claude_session.query מקבל tools="" לנטרול כל הכלים, ושני מחלצים (digest_metadata_extractor, bulletin_splitter) כבר משתמשים בו. כאן רק מיישרים את שאר המחלצים לאותו מסלול קנוני — אף קריאת חילוץ/שיפוט/סיווג טהורה לא צריכה כלי. מתוקנים (11 קריאות, 9 קבצים): halacha_extractor (×3: extract/NLI/consolidate), corroboration, claims_extractor, argument_aggregator, appraiser_facts_extractor, learning_loop, qa_validator, brainstorm, style_metadata_extractor. Invariants: מקיים INV-G2 (מסלול קנוני יחיד; סימטריה בין מחלצים-אחים) — לא מסלול מקביל חדש אלא שימוש עקבי בפרמטר הקיים. אין בליעה שקטה (§6) — נתיבי הכשל/retry נשמרים. ללא שינוי-ספ. בדיקות: 60/60 ב-tests/test_halacha_coerce.py + test_halacha_quality.py עוברות; py_compile נקי על כל 9 הקבצים. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
182 lines
6.0 KiB
Python
182 lines
6.0 KiB
Python
"""סיעור מוחות לגיבוש כיוון ההחלטה.
|
||
|
||
שלב 4א באיפיון המוצר:
|
||
1. הצגת טענות מרכזיות
|
||
2. הצעת 2-3 כיוונים אפשריים לנימוק
|
||
3. שיח אינטראקטיבי עד לכיוון מוסכם
|
||
4. יצירת מסמך כיוון
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
from uuid import UUID
|
||
|
||
from legal_mcp import config
|
||
from legal_mcp.config import parse_llm_json
|
||
from legal_mcp.services import db, claude_session
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
BRAINSTORM_PROMPT = """אתה יועץ משפטי מומחה בתכנון ובניה. תפקידך לסייע בגיבוש כיוון להחלטת ועדת ערר.
|
||
|
||
## הנחיות:
|
||
1. **הצג את הטענות המרכזיות** מכל הצדדים — 3-5 טענות עיקריות
|
||
2. **הצע 2-3 כיוונים אפשריים** לנימוק ההחלטה. כל כיוון כולל:
|
||
- שם הכיוון (שורה אחת)
|
||
- נימוקים מרכזיים (2-3 נקודות)
|
||
- פסיקה רלוונטית (אם יש)
|
||
- חוזקות וחולשות
|
||
3. **אל תמליץ** על כיוון אחד — הצג אותם באופן ניטרלי
|
||
4. **סוג הערר** משפיע על הטון: רישוי = חם יחסית, השבחה/פיצויים = קר ומקצועי
|
||
|
||
## תוצאה שנקבעה: {outcome_hebrew}
|
||
{reasoning_context}
|
||
|
||
## פלט:
|
||
החזר JSON בפורמט:
|
||
{{
|
||
"key_claims": [
|
||
{{"party": "עוררים/משיבים", "claim": "טענה מרכזית", "strength": "חזקה/בינונית/חלשה"}}
|
||
],
|
||
"directions": [
|
||
{{
|
||
"name": "שם הכיוון",
|
||
"reasoning": ["נימוק 1", "נימוק 2"],
|
||
"precedents": ["פסיקה רלוונטית"],
|
||
"strengths": ["חוזקה"],
|
||
"weaknesses": ["חולשה"]
|
||
}}
|
||
],
|
||
"recommended_order": "סדר מומלץ של נימוקים בדיון"
|
||
}}
|
||
"""
|
||
|
||
OUTCOME_HEBREW = {
|
||
"rejected": "דחייה",
|
||
"accepted": "קבלה",
|
||
"partial": "קבלה חלקית",
|
||
}
|
||
|
||
APPEAL_TYPE_TONE = {
|
||
"licensing": "טון חם יחסית — יש הקשר תכנוני רחב ואלמנטים אנושיים",
|
||
"betterment": "טון קר ומקצועי — יבש, ללא רגשות",
|
||
"compensation": "טון קר ומקצועי — דומה להיטל השבחה",
|
||
}
|
||
|
||
|
||
async def generate_directions(
|
||
case_id: UUID,
|
||
outcome: str,
|
||
reasoning: str = "",
|
||
) -> dict:
|
||
"""סיעור מוחות — הצגת טענות מרכזיות והצעת כיוונים.
|
||
|
||
Args:
|
||
case_id: מזהה התיק
|
||
outcome: תוצאה (rejected/accepted/partial)
|
||
reasoning: נימוק ראשוני (אם יש)
|
||
|
||
Returns:
|
||
dict עם key_claims, directions, recommended_order
|
||
"""
|
||
# Gather context
|
||
case = await db.get_case(case_id)
|
||
if not case:
|
||
raise ValueError(f"Case {case_id} not found")
|
||
|
||
claims = await db.get_claims(case_id)
|
||
docs = await db.list_documents(case_id)
|
||
|
||
# Build claims summary
|
||
claims_text = ""
|
||
if claims:
|
||
for c in claims:
|
||
role_heb = {"appellant": "עוררים", "respondent": "משיבים",
|
||
"committee": "ועדה מקומית", "permit_applicant": "מבקשי היתר"}.get(c["party_role"], c["party_role"])
|
||
claims_text += f"- [{role_heb}] {c['claim_text'][:200]}\n"
|
||
|
||
# Get document excerpts for context
|
||
doc_context = ""
|
||
for doc in docs[:5]:
|
||
text = await db.get_document_text(UUID(doc["id"]))
|
||
if text:
|
||
doc_context += f"\n--- {doc['title']} ({doc['doc_type']}) ---\n{text[:3000]}\n"
|
||
|
||
# Determine appeal type tone
|
||
appeal_type = case.get("appeal_type", "")
|
||
tone_hint = APPEAL_TYPE_TONE.get(appeal_type, "")
|
||
|
||
outcome_hebrew = OUTCOME_HEBREW.get(outcome, outcome)
|
||
reasoning_context = f"נימוק ראשוני: {reasoning}" if reasoning else "לא סופק נימוק — יש להציע כיוונים."
|
||
|
||
prompt = BRAINSTORM_PROMPT.format(
|
||
outcome_hebrew=outcome_hebrew,
|
||
reasoning_context=reasoning_context,
|
||
)
|
||
|
||
if tone_hint:
|
||
prompt += f"\n\n## טון: {tone_hint}"
|
||
|
||
user_content = f"""{prompt}
|
||
|
||
## פרטי התיק:
|
||
- מספר: {case['case_number']}
|
||
- נושא: {case.get('subject', '')}
|
||
- עוררים: {', '.join(case.get('appellants', []))}
|
||
- משיבים: {', '.join(case.get('respondents', []))}
|
||
|
||
## טענות שחולצו:
|
||
{claims_text or '(לא חולצו טענות עדיין)'}
|
||
|
||
## חומרי המקור:
|
||
{doc_context or '(אין מסמכים בתיק)'}
|
||
"""
|
||
|
||
result = await claude_session.query_json(user_content, tools="") # no tool_use → no error_max_turns
|
||
if result is None:
|
||
logger.warning("Failed to parse brainstorm response")
|
||
return {
|
||
"key_claims": [],
|
||
"directions": [],
|
||
"recommended_order": "",
|
||
"raw_response": "",
|
||
}
|
||
|
||
return result
|
||
|
||
|
||
def build_direction_doc(
|
||
outcome: str,
|
||
reasoning: str,
|
||
directions_result: dict,
|
||
selected_direction: int | None = None,
|
||
additional_notes: str = "",
|
||
) -> dict:
|
||
"""בניית מסמך כיוון מתוצאות סיעור המוחות.
|
||
|
||
Args:
|
||
outcome: תוצאה שנקבעה
|
||
reasoning: נימוק
|
||
directions_result: תוצאות מ-generate_directions
|
||
selected_direction: אינדקס הכיוון שנבחר (0-based)
|
||
additional_notes: הערות נוספות מהמשתמש
|
||
"""
|
||
direction = None
|
||
if selected_direction is not None and directions_result.get("directions"):
|
||
directions = directions_result["directions"]
|
||
if 0 <= selected_direction < len(directions):
|
||
direction = directions[selected_direction]
|
||
|
||
return {
|
||
"outcome": outcome,
|
||
"outcome_hebrew": OUTCOME_HEBREW.get(outcome, outcome),
|
||
"reasoning": reasoning,
|
||
"selected_direction": direction,
|
||
"key_claims": directions_result.get("key_claims", []),
|
||
"recommended_order": directions_result.get("recommended_order", ""),
|
||
"additional_notes": additional_notes,
|
||
"approved": True,
|
||
}
|