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

@@ -0,0 +1,165 @@
"""מדדי הצלחה (KPIs) לתהליך כתיבת החלטות.
מדדים:
1. אחוז שינוי — השוואת טיוטה לגרסה סופית (יעד: <10%)
2. אפס הזיות — ספירת הפניות לא מבוססות
3. מענה לכל טענה — כיסוי טענות בדיון
4. משקלות בטווח — עמידה ביחסי הזהב
5. רקע ניטרלי — ללא מילות שיפוט
6. זמן עיבוד — מקליטה עד טיוטה
"""
from __future__ import annotations
import json
import logging
from datetime import datetime
from uuid import UUID
from legal_mcp.services import db
logger = logging.getLogger(__name__)
async def get_case_metrics(case_id: UUID) -> dict:
"""חישוב מדדים לתיק בודד."""
case = await db.get_case(case_id)
if not case:
raise ValueError(f"Case {case_id} not found")
decision = await db.get_decision_by_case(case_id)
pool = await db.get_pool()
metrics = {
"case_number": case["case_number"],
"title": case.get("title", ""),
"status": case.get("status", ""),
}
# 1. Change percentage (if final version exists)
if decision and decision.get("status") == "final":
async with pool.acquire() as conn:
# Get draft word count
draft_words = await conn.fetchval(
"SELECT SUM(word_count) FROM decision_blocks WHERE decision_id = $1",
UUID(decision["id"]),
)
metrics["draft_words"] = draft_words or 0
# Change percent is stored during learning loop
metrics["change_percent"] = None # populated from learning_loop results
else:
metrics["draft_words"] = 0
metrics["change_percent"] = None
# 2. QA results
async with pool.acquire() as conn:
qa_rows = await conn.fetch(
"SELECT check_name, passed, severity, errors FROM qa_results WHERE case_id = $1",
case_id,
)
if qa_rows:
qa_results = {}
for row in qa_rows:
errors = json.loads(row["errors"]) if isinstance(row["errors"], str) else row["errors"]
qa_results[row["check_name"]] = {
"passed": row["passed"],
"severity": row["severity"],
"error_count": len(errors) if errors else 0,
}
metrics["qa"] = qa_results
metrics["qa_passed"] = all(r["passed"] for r in qa_results.values())
metrics["qa_critical_failures"] = sum(
1 for r in qa_results.values()
if not r["passed"] and r["severity"] == "critical"
)
else:
metrics["qa"] = None
metrics["qa_passed"] = None
# 3. Claims coverage
claims = await db.get_claims(case_id)
metrics["total_claims"] = len(claims)
# 4. Documents
docs = await db.list_documents(case_id)
metrics["total_documents"] = len(docs)
# 5. Processing time
if docs and decision:
first_doc_time = min(
d.get("created_at", datetime.max) for d in docs
if d.get("created_at")
)
decision_time = decision.get("created_at")
if first_doc_time and decision_time:
delta = decision_time - first_doc_time
metrics["processing_hours"] = round(delta.total_seconds() / 3600, 1)
else:
metrics["processing_hours"] = None
else:
metrics["processing_hours"] = None
return metrics
async def get_dashboard() -> dict:
"""דשבורד כולל — סיכום מדדים על כל התיקים."""
pool = await db.get_pool()
async with pool.acquire() as conn:
# Case counts by status
status_rows = await conn.fetch(
"SELECT status, COUNT(*) as cnt FROM cases GROUP BY status ORDER BY cnt DESC"
)
cases_by_status = {r["status"]: r["cnt"] for r in status_rows}
# Total counts
total_cases = await conn.fetchval("SELECT COUNT(*) FROM cases")
total_docs = await conn.fetchval("SELECT COUNT(*) FROM documents")
total_claims = await conn.fetchval("SELECT COUNT(*) FROM claims")
total_chunks = await conn.fetchval("SELECT COUNT(*) FROM document_chunks")
total_decisions = await conn.fetchval("SELECT COUNT(*) FROM decisions")
total_corpus = await conn.fetchval("SELECT COUNT(*) FROM style_corpus")
total_patterns = await conn.fetchval("SELECT COUNT(*) FROM style_patterns")
total_case_law = await conn.fetchval("SELECT COUNT(*) FROM case_law")
# QA summary
qa_total = await conn.fetchval("SELECT COUNT(DISTINCT case_id) FROM qa_results")
qa_passed = await conn.fetchval(
"""SELECT COUNT(DISTINCT case_id) FROM qa_results
WHERE case_id NOT IN (
SELECT case_id FROM qa_results WHERE passed = false AND severity = 'critical'
)"""
)
# Final decisions
final_count = await conn.fetchval(
"SELECT COUNT(*) FROM decisions WHERE status = 'final'"
)
# Average words per decision
avg_words = await conn.fetchval(
"SELECT AVG(total_words) FROM decisions WHERE total_words > 0"
)
return {
"summary": {
"total_cases": total_cases,
"total_documents": total_docs,
"total_claims": total_claims,
"total_chunks": total_chunks,
"total_decisions": total_decisions,
"final_decisions": final_count,
"style_corpus": total_corpus,
"style_patterns": total_patterns,
"case_law_entries": total_case_law,
},
"cases_by_status": cases_by_status,
"qa": {
"cases_validated": qa_total,
"cases_passed": qa_passed,
"pass_rate": round(qa_passed / qa_total * 100, 1) if qa_total else None,
},
"avg_decision_words": round(avg_words) if avg_words else None,
}