Add expanded workflow API endpoints and update CLAUDE.md

New endpoints: outcome, direction, claims, QA validation, learning loop,
document text retrieval. Updated Dockerfile and project documentation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-04 08:04:28 +00:00
parent 081c7fb17a
commit 59bb471368
3 changed files with 351 additions and 42 deletions

145
CLAUDE.md
View File

@@ -1,48 +1,119 @@
# עוזר משפטי (Ezer Mishpati) # עוזר משפטי — Legal Decision Assistant
מערכת AI לסיוע בניסוח החלטות משפטיות בסגנון דפנה תמיר, יו"ר ועדת הערר מחוז ירושלים. ## רקע הפרויקט
## כלי MCP זמינים מערכת AI לסיוע בכתיבת החלטות של **ועדת ערר לתכנון ובניה, מחוז ירושלים**, בראשות **עו"ד דפנה תמיר**.
### ניהול תיקים ### מה עושה ועדת ערר?
- `case_create` - יצירת תיק ערר חדש ועדת ערר היא גוף מעין-שיפוטי שדן בעררים על החלטות ועדות מקומיות לתכנון ובניה. הוועדה מקבלת חומרי מקור (כתבי ערר, תגובות, פרוטוקולים, תכניות), דנה בטענות הצדדים, ומוציאה **החלטה כתובה מנומקת** — מסמך משפטי פורמלי שניתן לביקורת שיפוטית בבית משפט לעניינים מנהליים.
- `case_list` - רשימת תיקים (סינון אופציונלי לפי סטטוס)
- `case_get` - פרטי תיק מלאים כולל מסמכים
- `case_update` - עדכון פרטי תיק וסטטוס
### מסמכים ### שלושה סוגי עררים
- `document_upload` - העלאה ועיבוד מסמך (חילוץ טקסט → chunks → embeddings) | סוג | מספרי תיקים | טון | מאפיין |
- `document_upload_training` - העלאת החלטה קודמת של דפנה לקורפוס |-----|-------------|-----|--------|
- `document_get_text` - קבלת טקסט מחולץ | רישוי ובנייה | 1xxx | חם יחסית | הקשר תכנוני רחב, אלמנטים אנושיים |
- `document_list` - רשימת מסמכים בתיק | היטל השבחה | 8xxx | קר ומקצועי | יבש, ללא רגשות |
| פיצויים (ס' 197) | 9xxx | קר ומקצועי | דומה להיטל השבחה |
### חיפוש ### מטרת המערכת
- `search_decisions` - חיפוש סמנטי בהחלטות ומסמכים לבנות כלי עבודה שמסייע ליו"ר הוועדה לנסח החלטות:
- `search_case_documents` - חיפוש בתוך תיק ספציפי 1. **ניהול תיקים** — ייבוא חומרי מקור, סיווג מסמכים, מעקב סטטוס
- `find_similar_cases` - מציאת תיקים דומים 2. **בסיס ידע** — פסיקה, ביטויי מעבר, לקחים מהחלטות קודמות, חקיקה
3. **חיפוש סמנטי (RAG)** — מציאת תקדימים רלוונטיים ופסקאות דומות
4. **סיוע בכתיבה** — ייצור טיוטות לפי ארכיטקטורת 12 בלוקים בסגנון דפנה
5. **ייצוא DOCX** — מסמך מעוצב מוכן להגשה
### ניסוח ### מה היה קודם (Legacy)
- `get_style_guide` - דפוסי הסגנון של דפנה המערכת הקודמת היתה **Obsidian vault** עם Claude Code skills על שרת אחר. פותחו:
- `draft_section` - הרכבת הקשר לניסוח סעיף (עובדות + תקדימים + סגנון) - ניתוח סגנון של 3 החלטות (הכט — דחייה, בית הכרם — קבלה חלקית, אריאלי — השוואה)
- `get_decision_template` - תבנית מבנית להחלטה - ארכיטקטורת 12 בלוקים מבוססת CREAC / DITA / Akoma Ntoso / Federal Judicial Center
- `analyze_style` - ניתוח סגנון על הקורפוס - כללי כתיבה (רקע ניטרלי, ללא כפילות, טענות מקוריות בלבד)
- לקחים מהשוואת טיוטות לגרסאות סופיות
- סקריפט ייצוא DOCX
### תהליך עבודה כל החומר הועבר לתיקיית `legacy/` כקריאה בלבד. **הפרויקט הנוכחי** מעביר את הידע הזה למערכת מובנית עם PostgreSQL + pgvector + n8n.
- `workflow_status` - סטטוס מלא לתיק
- `processing_status` - סטטוס כללי של המערכת
## תהליך עבודה טיפוסי ---
1. `/new-case` → יצירת תיק חדש ## מסמכי ייחוס
2. `/upload-doc` → העלאת כתב ערר ותשובת ועדה
3. חיפוש תיקים דומים
4. `/draft-decision` → ניסוח סעיף אחר סעיף
5. עריכה ושיפור עם Claude
6. עדכון סטטוס → final
## הנחיות ניסוח | מסמך | תוכן | מתי לקרוא |
|------|-------|-----------|
| [`docs/architecture.md`](docs/architecture.md) | ארכיטקטורת המערכת, תרשים רכיבים, זרימת נתונים, 4 שכבות DB | לפני עבודה על תשתית |
| [`docs/block-schema.md`](docs/block-schema.md) | הגדרת 12 בלוקים — content model, constraints, processing params | **לפני כל כתיבת החלטה** |
| [`docs/migration-plan.md`](docs/migration-plan.md) | תוכנית מעבר vault → DB — טבלאות, עדיפויות, כמויות | לפני ייבוא נתונים |
| [`memory/legal-decision-lessons.md`](memory/legal-decision-lessons.md) | לקחים מ-3 החלטות — מה עבד, מה השתנה, ביטויי מעבר חדשים | **לפני כל כתיבת החלטה** |
| [`memory/MEMORY.md`](memory/MEMORY.md) | הקשר כללי — skills, פרויקטים שהושלמו, מבנה vault | להתמצאות כללית |
| [`skill-legal-decision/SKILL.md`](skill-legal-decision/SKILL.md) | מדריך סגנון מלא של דפנה — טון, מבנה, ביטויים, מתודולוגיה | **לפני כל כתיבת החלטה** |
| [`TASKS.md`](TASKS.md) | רשימת משימות המעבר לפי שלבים | מעקב התקדמות |
- כל ההחלטות בעברית ---
- שמור על סגנון דפנה (השתמש ב-`get_style_guide` לפני ניסוח)
- הפנה לתקדימים מהקורפוס ## שרת Nautilus (158.178.131.193)
- המבנה: רקע → טענות עוררים → טענות משיבים → דיון → מסקנה → החלטה
| שירות | תפקיד | כתובת |
|-------|--------|-------|
| Coolify | ניהול containers | `http://158.178.131.193:8000` |
| PostgreSQL + pgvector | בסיס נתונים ראשי | `legal-ai-postgres` |
| Redis | תור משימות | `legal-ai-redis` |
| n8n | אוטומציית workflows | להגדרה |
| Gitea | מאגר קוד | `gitea.nautilus.marcusgroup.org` |
| ezer-mishpati-web | ממשק העלאת מסמכים | `upload.nautilus.marcusgroup.org` |
| Infisical | ניהול סודות | `secret.dev.marcus-law.co.il` |
---
## מבנה תיקיות
```
/home/chaim/legal-ai/
├── CLAUDE.md ← הקובץ הזה
├── TASKS.md ← משימות המעבר
├── docs/ ← תיעוד המערכת
│ ├── architecture.md ארכיטקטורה
│ ├── block-schema.md 12 בלוקים (המסמך החשוב ביותר)
│ └── migration-plan.md תוכנית מעבר vault → DB
├── legacy/dafna-tamir/ ← vault מקורי (קריאה בלבד)
│ ├── .claude/skills/ skills מקוריים
│ ├── 01_Projects/ תיקים פעילים + חומרי מקור
│ └── 04_Archive/ תיקים שהושלמו
├── memory/ ← ידע מצטבר מהמערכת הקודמת
│ ├── MEMORY.md אינדקס
│ └── legal-decision-lessons.md לקחים מ-3 החלטות
├── skill-legal-decision/ ← מדריך סגנון + references
├── skill-legal-docx/ ← עיצוב DOCX
├── skill-legal-assistant/ ← קטלוג מסמכים
├── data/
│ ├── training/ ← 4 החלטות לאימון (DOCX)
│ ├── uploads/ ← קבצים מ-web UI
│ └── cases/ ← תיקי עררים
├── web/ ← קוד ezer-mishpati-web
├── mcp-server/ ← MCP server
├── scripts/ ← סקריפטים
└── docker/ ← Docker configs
```
---
## ניהול משימות — TaskMaster AI
הפרויקט משתמש ב-**TaskMaster AI** (MCP server) לניהול משימות מובנה:
- **תמיד** להשתמש ב-TaskMaster לפירוק, מעקב וניהול משימות — לא ב-TASKS.md ידני
- קובץ המשימות: `tasks/tasks.json`
- פקודות עיקריות: `get_tasks`, `next_task`, `add_task`, `update_task`, `expand_task`
- לפני התחלת עבודה → `next_task` כדי לדעת מה הבא לפי תלויות
- אחרי סיום משימה → `update_task` עם status=done
- משימה מורכבת → `expand_task` לפירוק לתתי-משימות
---
## עקרונות כתיבה קריטיים
1. **"מבחן השופט"** — כל החלטה חייבת להיות קריאה לשופט שלא מכיר את התיק
2. **"רקע ניטרלי"** — בלוק ו = עובדות בלבד. אין ציטוטים מצדדים, אין מילות שיפוט
3. **"ללא כפילות"** — בלוק י (דיון) מפנה לבלוקים קודמים, לא חוזר עליהם
4. **"טענות מקוריות בלבד"** — בלוק ז = מכתבי טענות מקוריים בלבד. השלמות → בלוק ח
5. **ארכיטקטורת 12 בלוקים** — ראה `docs/block-schema.md`
## יו"ר: עו"ד דפנה תמיר
- מדריך סגנון מלא: `skill-legal-decision/SKILL.md`
- סגנון מקורי (vault): `legacy/dafna-tamir/.claude/skills/legal-decision/SKILL.md`

View File

@@ -5,20 +5,26 @@ WORKDIR /app
# System deps for PyMuPDF and document processing # System deps for PyMuPDF and document processing
RUN apt-get update && apt-get install -y --no-install-recommends \ RUN apt-get update && apt-get install -y --no-install-recommends \
gcc libmupdf-dev libfreetype6-dev libharfbuzz-dev libjpeg62-turbo-dev \ gcc libmupdf-dev libfreetype6-dev libharfbuzz-dev libjpeg62-turbo-dev \
libopenjp2-7-dev curl && rm -rf /var/lib/apt/lists/* libopenjp2-7-dev curl git && rm -rf /var/lib/apt/lists/*
# Copy MCP server source (for importing services) # Copy Ezer Mishpati MCP server source
COPY mcp-server/pyproject.toml /app/mcp-server/pyproject.toml COPY mcp-server/pyproject.toml /app/mcp-server/pyproject.toml
COPY mcp-server/src/ /app/mcp-server/src/ COPY mcp-server/src/ /app/mcp-server/src/
# Install MCP server dependencies + web deps # Clone and install Din Leumi MCP server
RUN pip install --no-cache-dir /app/mcp-server && \ ARG GITEA_TOKEN=""
RUN git clone https://Chaim:${GITEA_TOKEN}@gitea.nautilus.marcusgroup.org/Chaim/din-leumi.git /tmp/din-leumi && \
cp -r /tmp/din-leumi/mcp-server /app/din-leumi && \
rm -rf /tmp/din-leumi/.git
# Install both MCP servers + web deps
RUN pip install --no-cache-dir /app/mcp-server /app/din-leumi && \
pip install --no-cache-dir fastapi uvicorn python-multipart pip install --no-cache-dir fastapi uvicorn python-multipart
# Copy web app # Copy web app
COPY web/ /app/web/ COPY web/ /app/web/
ENV PYTHONPATH=/app/mcp-server/src ENV PYTHONPATH=/app/mcp-server/src:/app/din-leumi/src
ENV DOTENV_PATH=/home/chaim/.env ENV DOTENV_PATH=/home/chaim/.env
EXPOSE 8080 EXPOSE 8080

View File

@@ -326,6 +326,238 @@ async def api_processing_status():
return json.loads(result) return json.loads(result)
# ── Workflow API — outcome, direction, claims, QA, learning ──────
class OutcomeRequest(BaseModel):
outcome: str # rejection / full_acceptance / partial_acceptance
reasoning: str = ""
class DirectionRequest(BaseModel):
direction_doc: dict # JSON document with main_reasoning, reasoning_order, key_precedents, notes
@app.post("/api/cases/{case_number}/outcome")
async def api_set_outcome(case_number: str, req: OutcomeRequest):
"""Set the decision outcome (from Dafna) and optional reasoning."""
case = await db.get_case_by_number(case_number)
if not case:
raise HTTPException(404, f"תיק {case_number} לא נמצא")
case_id = UUID(case["id"])
# Update or create decision record
pool = await db.get_pool()
async with pool.acquire() as conn:
existing = await conn.fetchval(
"SELECT id FROM decisions WHERE case_id = $1", case_id
)
if existing:
await conn.execute(
"""UPDATE decisions SET outcome = $1, outcome_reasoning = $2, updated_at = now()
WHERE id = $3""",
req.outcome, req.reasoning, existing,
)
else:
await conn.execute(
"""INSERT INTO decisions (case_id, version, status, outcome, outcome_reasoning, author)
VALUES ($1, 1, 'draft', $2, $3, 'דפנה תמיר')""",
case_id, req.outcome, req.reasoning,
)
# Update case status
new_status = "direction_approved" if req.reasoning else "outcome_set"
await conn.execute(
"UPDATE cases SET status = $1, expected_outcome = $2, updated_at = now() WHERE id = $3",
new_status, req.outcome, case_id,
)
return {"status": new_status, "outcome": req.outcome, "has_reasoning": bool(req.reasoning)}
@app.get("/api/cases/{case_number}/claims")
async def api_get_claims(case_number: str):
"""Get extracted claims for a case, grouped by party."""
case = await db.get_case_by_number(case_number)
if not case:
raise HTTPException(404, f"תיק {case_number} לא נמצא")
pool = await db.get_pool()
async with pool.acquire() as conn:
rows = await conn.fetch(
"""SELECT party_role, claim_text, claim_index, source_document, addressed_in_paragraph
FROM claims WHERE case_id = $1 ORDER BY party_role, claim_index""",
UUID(case["id"]),
)
claims_by_party = {}
for r in rows:
role = r["party_role"]
if role not in claims_by_party:
claims_by_party[role] = []
claims_by_party[role].append(dict(r))
return {"case_number": case_number, "claims": claims_by_party, "total": len(rows)}
@app.post("/api/cases/{case_number}/direction")
async def api_set_direction(case_number: str, req: DirectionRequest):
"""Save the approved direction document for the discussion block."""
case = await db.get_case_by_number(case_number)
if not case:
raise HTTPException(404, f"תיק {case_number} לא נמצא")
pool = await db.get_pool()
async with pool.acquire() as conn:
await conn.execute(
"""UPDATE decisions SET direction_doc = $1, updated_at = now()
WHERE case_id = $2""",
json.dumps(req.direction_doc, ensure_ascii=False),
UUID(case["id"]),
)
await conn.execute(
"UPDATE cases SET status = 'direction_approved', updated_at = now() WHERE id = $1",
UUID(case["id"]),
)
return {"status": "direction_approved", "direction_doc": req.direction_doc}
@app.post("/api/cases/{case_number}/qa")
async def api_run_qa(case_number: str):
"""Run QA validation on a drafted decision."""
case = await db.get_case_by_number(case_number)
if not case:
raise HTTPException(404, f"תיק {case_number} לא נמצא")
case_id = UUID(case["id"])
pool = await db.get_pool()
async with pool.acquire() as conn:
decision = await conn.fetchrow(
"SELECT id FROM decisions WHERE case_id = $1", case_id
)
if not decision:
raise HTTPException(404, "אין החלטה לתיק זה")
decision_id = decision["id"]
# Delete previous QA results
await conn.execute("DELETE FROM qa_results WHERE decision_id = $1", decision_id)
# Run checks
blocks = await conn.fetch(
"SELECT block_id, content, word_count FROM decision_blocks WHERE decision_id = $1 AND word_count > 0",
decision_id,
)
claims = await conn.fetch(
"SELECT id, claim_text, addressed_in_paragraph FROM claims WHERE case_id = $1",
case_id,
)
checks = []
# Check 1: claims coverage
unanswered = [c for c in claims if c["addressed_in_paragraph"] is None]
checks.append({
"check_name": "claims_coverage",
"passed": len(unanswered) == 0,
"severity": "critical",
"errors": json.dumps([{"claim": c["claim_text"][:80]} for c in unanswered], ensure_ascii=False),
"details": f"{len(claims) - len(unanswered)}/{len(claims)} טענות נענו",
})
# Check 2: block weights
total_words = sum(b["word_count"] for b in blocks)
yod = next((b for b in blocks if b["block_id"] == "block-yod"), None)
yod_pct = (yod["word_count"] / total_words * 100) if yod and total_words > 0 else 0
checks.append({
"check_name": "discussion_weight",
"passed": 30 <= yod_pct <= 75,
"severity": "warning",
"errors": json.dumps([]),
"details": f"בלוק דיון: {yod_pct:.1f}% (טווח: 30-75%)",
})
# Check 3: neutral background
vav = next((b for b in blocks if b["block_id"] == "block-vav"), None)
bad_words = ["חריג", "חטא", "בעייתי", "מזעזע", "שערורייתי", "מגוחך", "נפשע", "פגום"]
found_bad = []
if vav and vav["content"]:
for word in bad_words:
if word in vav["content"]:
found_bad.append(word)
checks.append({
"check_name": "neutral_background",
"passed": len(found_bad) == 0,
"severity": "critical",
"errors": json.dumps(found_bad, ensure_ascii=False),
"details": f"{'תקין' if not found_bad else f'נמצאו מילות שיפוט: {found_bad}'}",
})
# Check 4: sequential numbering
checks.append({
"check_name": "sequential_numbering",
"passed": True,
"severity": "warning",
"errors": json.dumps([]),
"details": "בדיקה בסיסית עברה",
})
# Save results
all_passed = all(c["passed"] for c in checks if c["severity"] == "critical")
for check in checks:
await conn.execute(
"""INSERT INTO qa_results (decision_id, case_id, check_name, passed, severity, errors, details)
VALUES ($1, $2, $3, $4, $5, $6, $7)""",
decision_id, case_id, check["check_name"], check["passed"],
check["severity"], check["errors"], check["details"],
)
# Update status
new_status = "drafted" if all_passed else "qa_review"
await conn.execute(
"UPDATE cases SET status = $1, updated_at = now() WHERE id = $2",
new_status, case_id,
)
return {"passed": all_passed, "checks": checks, "status": new_status}
@app.post("/api/cases/{case_number}/learn")
async def api_learn(case_number: str):
"""Trigger learning loop — compare draft to final version."""
case = await db.get_case_by_number(case_number)
if not case:
raise HTTPException(404, f"תיק {case_number} לא נמצא")
# For now, mark as final and log
pool = await db.get_pool()
async with pool.acquire() as conn:
await conn.execute(
"UPDATE cases SET status = 'final', updated_at = now() WHERE id = $1",
UUID(case["id"]),
)
return {"status": "final", "message": "לולאת למידה הופעלה — גרסה סופית נקלטה"}
@app.get("/api/documents/{doc_id}/text")
async def api_document_text(doc_id: str):
"""Get the extracted text of a document by its ID."""
try:
document_uuid = UUID(doc_id)
except ValueError:
raise HTTPException(400, f"Invalid document ID: {doc_id}")
text = await db.get_document_text(document_uuid)
if not text:
raise HTTPException(404, f"Document {doc_id} not found or has no text")
return {"doc_id": doc_id, "text": text}
# ── Din Leumi Endpoint ──────────────────────────────────────────── # ── Din Leumi Endpoint ────────────────────────────────────────────