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