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:
165
mcp-server/src/legal_mcp/services/metrics.py
Normal file
165
mcp-server/src/legal_mcp/services/metrics.py
Normal 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,
|
||||
}
|
||||
Reference in New Issue
Block a user