Add full decision writing pipeline: classify, extract, brainstorm, write, QA, export
New services (11 files): - classifier.py: auto doc-type classification + party identification (Claude Haiku) - claims_extractor.py: claim extraction from pleadings (Claude Sonnet + regex) - references_extractor.py: plan/case-law/legislation detection (regex) - brainstorm.py: direction generation with 2-3 options (Claude Sonnet) - block_writer.py: 12-block decision writer (template + Claude Sonnet/Opus) - docx_exporter.py: DOCX export with David font, RTL, headings - qa_validator.py: 6 QA checks with export blocking on critical failure - learning_loop.py: draft vs final comparison + lesson extraction - metrics.py: KPIs dashboard per case and global - audit.py: action audit log - cli.py: standalone CLI with 11 commands Updated pipeline: extract → classify → chunk → embed → store → extract_references New MCP tools: 29 total (was 16) New DB tables: audit_log, decisions CRUD, claims CRUD Config: Infisical support, external service allowlist Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
206
mcp-server/src/legal_mcp/services/brainstorm.py
Normal file
206
mcp-server/src/legal_mcp/services/brainstorm.py
Normal file
@@ -0,0 +1,206 @@
|
||||
"""סיעור מוחות לגיבוש כיוון ההחלטה.
|
||||
|
||||
שלב 4א באיפיון המוצר:
|
||||
1. הצגת טענות מרכזיות
|
||||
2. הצעת 2-3 כיוונים אפשריים לנימוק
|
||||
3. שיח אינטראקטיבי עד לכיוון מוסכם
|
||||
4. יצירת מסמך כיוון
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from uuid import UUID
|
||||
|
||||
import anthropic
|
||||
|
||||
from legal_mcp import config
|
||||
from legal_mcp.services import db
|
||||
|
||||
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
|
||||
|
||||
|
||||
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 '(אין מסמכים בתיק)'}
|
||||
"""
|
||||
|
||||
client = _get_anthropic()
|
||||
message = client.messages.create(
|
||||
model="claude-sonnet-4-20250514",
|
||||
max_tokens=4096,
|
||||
messages=[{"role": "user", "content": user_content}],
|
||||
)
|
||||
|
||||
raw = message.content[0].text.strip()
|
||||
try:
|
||||
import re
|
||||
json_match = re.search(r"\{.*\}", raw, re.DOTALL)
|
||||
if json_match:
|
||||
result = json.loads(json_match.group())
|
||||
else:
|
||||
result = json.loads(raw)
|
||||
except json.JSONDecodeError:
|
||||
logger.warning("Failed to parse brainstorm response: %s", raw[:300])
|
||||
return {
|
||||
"key_claims": [],
|
||||
"directions": [],
|
||||
"recommended_order": "",
|
||||
"raw_response": raw,
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
Reference in New Issue
Block a user