Files
legal-ai/mcp-server/src/legal_mcp/services/brainstorm.py
Chaim d05c1e3fce
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 5s
fix(extractors): disable tools on text→JSON claude_session calls (no error_max_turns)
כל קריאות 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>
2026-06-11 11:49:35 +00:00

182 lines
6.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""סיעור מוחות לגיבוש כיוון ההחלטה.
שלב 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,
}