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:
2026-04-03 10:21:47 +00:00
parent df7cc4f5a5
commit d9e5ef0f46
21 changed files with 3957 additions and 14 deletions

View File

@@ -332,9 +332,144 @@ async def get_decision_template(case_number: str) -> str:
return template
async def validate_decision(case_number: str) -> str:
"""בדיקת QA אוטומטית על ההחלטה — 6 בדיקות. אם נכשלת בדיקה קריטית — ייצוא חסום.
Args:
case_number: מספר תיק הערר
"""
from legal_mcp.services import qa_validator
case = await db.get_case_by_number(case_number)
if not case:
return f"תיק {case_number} לא נמצא."
case_id = UUID(case["id"])
try:
result = await qa_validator.validate_decision(case_id)
return json.dumps(result, default=str, ensure_ascii=False, indent=2)
except ValueError as e:
return json.dumps({"status": "error", "message": str(e)}, ensure_ascii=False, indent=2)
async def export_docx(case_number: str, output_path: str = "") -> str:
"""ייצוא החלטה לקובץ DOCX מעוצב — גופן David, RTL, כותרות, מספור סעיפים.
Args:
case_number: מספר תיק הערר
output_path: נתיב לשמירה (אופציונלי — ברירת מחדל: תיקיית התיק)
"""
from legal_mcp.services import docx_exporter
case = await db.get_case_by_number(case_number)
if not case:
return f"תיק {case_number} לא נמצא."
case_id = UUID(case["id"])
try:
path = await docx_exporter.export_decision(case_id, output_path or None)
return json.dumps({
"status": "completed",
"path": path,
"message": f"DOCX נוצר: {path}",
}, ensure_ascii=False, indent=2)
except ValueError as e:
return json.dumps({
"status": "error",
"message": str(e),
}, ensure_ascii=False, indent=2)
async def analyze_style() -> str:
"""הרצת ניתוח סגנון על קורפוס ההחלטות של דפנה. מחלץ דפוסי כתיבה ושומר אותם."""
from legal_mcp.services.style_analyzer import analyze_corpus
result = await analyze_corpus()
return json.dumps(result, ensure_ascii=False, indent=2)
async def write_block(
case_number: str,
block_id: str,
instructions: str = "",
) -> str:
"""כתיבת בלוק יחיד בהחלטה. כותב ושומר ב-DB.
Args:
case_number: מספר תיק הערר
block_id: מזהה הבלוק: block-alef, block-bet, block-gimel, block-dalet, block-he, block-vav, block-zayin, block-chet, block-tet, block-yod, block-yod-alef, block-yod-bet
instructions: הנחיות נוספות לניסוח
"""
from legal_mcp.services import block_writer
case = await db.get_case_by_number(case_number)
if not case:
return f"תיק {case_number} לא נמצא."
case_id = UUID(case["id"])
try:
result = await block_writer.write_and_store_block(case_id, block_id, instructions)
return json.dumps(result, default=str, ensure_ascii=False, indent=2)
except ValueError as e:
return str(e)
async def write_all_blocks(
case_number: str,
start_from: str = "block-alef",
instructions: str = "",
) -> str:
"""כתיבת כל הבלוקים בהחלטה, בלוק אחרי בלוק. שומר כל בלוק מיד אחרי כתיבה.
Args:
case_number: מספר תיק הערר
start_from: מאיזה בלוק להתחיל (ברירת מחדל: block-alef)
instructions: הנחיות כלליות
"""
from legal_mcp.services import block_writer
from legal_mcp.services.block_writer import BLOCK_CONFIG
case = await db.get_case_by_number(case_number)
if not case:
return f"תיק {case_number} לא נמצא."
case_id = UUID(case["id"])
# Determine start index
start_idx = BLOCK_CONFIG.get(start_from, {}).get("index", 1)
results = []
block_order = sorted(BLOCK_CONFIG.items(), key=lambda x: x[1]["index"])
for bid, cfg in block_order:
if cfg["index"] < start_idx:
continue
try:
result = await block_writer.write_and_store_block(case_id, bid, instructions)
results.append({
"block_id": bid,
"title": result["title"],
"word_count": result["word_count"],
"status": "completed",
})
except ValueError as e:
results.append({
"block_id": bid,
"title": cfg["title"],
"status": "error",
"error": str(e),
})
# Stop on critical error (e.g., missing direction for block-yod)
if "כיוון מאושר" in str(e):
break
total_words = sum(r.get("word_count", 0) for r in results)
return json.dumps({
"blocks": results,
"total_words": total_words,
"completed": sum(1 for r in results if r["status"] == "completed"),
}, default=str, ensure_ascii=False, indent=2)