Improve document processing pipeline and agent workflows

- Add delete_document_chunks for reprocessing, save extracted text to disk
- Expand case directory structure (original/extracted/proofread/backup)
- Update classifier patterns (תגובה, הודעת עמדה)
- Fix proofreader agent paths for new directory layout
- Update HEARTBEAT to notify on every task completion
- Improve bidi_table with LRE/PDF directional embedding
- Add Paperclip project verification and auto-close setup issue
- Add auto-sync-cases.sh for Gitea synchronization

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-09 16:45:49 +00:00
parent 63c9ca184b
commit 3f759d3610
10 changed files with 164 additions and 19 deletions

View File

@@ -69,16 +69,17 @@ python3 /home/chaim/legal-ai/scripts/notify.py \
"תוכן ההודעה עם סיכום מה נדרש" "תוכן ההודעה עם סיכום מה נדרש"
``` ```
**מתי לשלוח:** **מתי לשלוח — תמיד:**
- **סיום כל משימה** — עם סיכום קצר של מה בוצע
- בקשה לקביעת תוצאה (דחייה/קבלה/חלקית) - בקשה לקביעת תוצאה (דחייה/קבלה/חלקית)
- בקשה לאישור כיוון נימוק - בקשה לאישור כיוון נימוק
- דוח QA שנכשל (צריך החלטה על תיקונים) - דוח QA שנכשל (צריך החלטה על תיקונים)
- החלטה מוכנה לביקורת דפנה - החלטה מוכנה לביקורת דפנה
- כל מצב שדורש פעולה אנושית ולא יכול להתקדם לבד - כל מצב שדורש פעולה אנושית ולא יכול להתקדם לבד
- שגיאה שלא ניתן לפתור ללא התערבות
**מתי לא לשלוח:** **מתי לא לשלוח:**
- עדכוני סטטוס רגילים - עדכוני סטטוס ביניים (רק בסיום)
- סיום משימה שלא דורשת תגובה
- שגיאות טכניות שאפשר לפתור לבד - שגיאות טכניות שאפשר לפתור לבד
## 6. Release ## 6. Release

View File

@@ -183,9 +183,9 @@ tools:
1. [שאלה עקרונית — "האם..."] 1. [שאלה עקרונית — "האם..."]
2. [שאלה יישומית — "מהם..."] 2. [שאלה יישומית — "מהם..."]
**מילות מפתח לחיפוש:** **חיפוש תקדימים:**
- nevo: "ביטוי" ו "ביטוי" ו "ועדת ערר" - nevo (קלאסי): "ביטוי" ו "ביטוי" ו "ועדת ערר"
- law-mate: מילה1 מילה2 מילה3 - nevo AI / law-mate: [השאלות המשפטיות מלמעלה — שאלה עקרונית + יישומית]
**חקיקה רלוונטית:** **חקיקה רלוונטית:**
- סעיף X לחוק... - סעיף X לחוק...

View File

@@ -42,7 +42,7 @@ tools:
1. טען את מילון ראשי התיבות: `/home/chaim/legal-ai/data/abbreviations.json` 1. טען את מילון ראשי התיבות: `/home/chaim/legal-ai/data/abbreviations.json`
2. **סדר החלפה:** ארוכים לפני קצרים (למניעת החלפה חלקית) 2. **סדר החלפה:** ארוכים לפני קצרים (למניעת החלפה חלקית)
3. לכל מסמך: 3. לכל מסמך:
- קרא את קובץ ה-MD מהדיסק (מצא אותו ב-`data/cases/` לפי הנתיב) - קרא את קובץ הטקסט מתיקיית `documents/extracted/` בתיק (קובץ `.txt` עם אותו שם כמו ה-PDF המקורי)
- החלף כל מופע של ראשי תיבות שבורים (מפתחות המילון) בצורה הנכונה (ערכי המילון) - החלף כל מופע של ראשי תיבות שבורים (מפתחות המילון) בצורה הנכונה (ערכי המילון)
- ספור כמה החלפות בוצעו - ספור כמה החלפות בוצעו
@@ -58,13 +58,13 @@ tools:
**תקן** רק מה שאתה בטוח בו (90%+). אם לא בטוח — סמן `[?]` ליד המקום הבעייתי. **תקן** רק מה שאתה בטוח בו (90%+). אם לא בטוח — סמן `[?]` ליד המקום הבעייתי.
### שלב 4: שמירה ### שלב 4: שמירה
1. **גיבוי**: שמור עותק מקורי כ-`{filename}.pre-proofread.md` 1. **גיבוי**: העתק את הקובץ המקורי מ-`extracted/` לתיקיית `documents/backup/` עם סיומת `.pre-proofread.txt`
2. **כתוב** את הגרסה המתוקנת לקובץ ה-MD המקורי 2. **כתוב** את הגרסה המתוקנת לתיקיית `documents/proofread/` (עם אותו שם קובץ כמו ב-`extracted/`)
3. עדכן את מסד הנתונים — שנה `extraction_status` ל-`proofread`: 3. עדכן את מסד הנתונים — שנה `extraction_status` ל-`proofread`:
```bash ```bash
PGPASSWORD="${PGPASSWORD:-$(grep DB_PASSWORD /home/chaim/.env | cut -d= -f2)}" \ PGPASSWORD="${PGPASSWORD:-$(grep DB_PASSWORD /home/chaim/.env | cut -d= -f2)}" \
psql -h localhost -p 5432 -U "${DB_USER:-legal_ai}" -d "${DB_NAME:-legal_ai}" \ psql -h localhost -p 5432 -U "${DB_USER:-legal_ai}" -d "${DB_NAME:-legal_ai}" \
-c "UPDATE documents SET extraction_status = 'proofread', extracted_text = pg_read_file('/path/to/file.md') WHERE id = '{doc_id}';" -c "UPDATE documents SET extraction_status = 'proofread', extracted_text = pg_read_file('/path/to/file.txt') WHERE id = '{doc_id}';"
``` ```
אם עדכון DB לא אפשרי, עדכן רק את הקובץ ודווח. אם עדכון DB לא אפשרי, עדכן רק את הקובץ ודווח.
@@ -90,7 +90,7 @@ psql -h localhost -p 5432 -U "${DB_USER:-legal_ai}" -d "${DB_NAME:-legal_ai}" \
## כללים קריטיים ## כללים קריטיים
1. **אל תשנה תוכן משפטי** — רק תיקוני OCR. אם מילה נראית מוזרה אבל היא מונח משפטי — אל תגע 1. **אל תשנה תוכן משפטי** — רק תיקוני OCR. אם מילה נראית מוזרה אבל היא מונח משפטי — אל תגע
2. **אל תדרוס בלי גיבוי** — תמיד `.pre-proofread.md` לפני שינוי 2. **אל תדרוס בלי גיבוי** — תמיד העתק ל-`backup/` לפני שינוי
3. **ראשי תיבות ארוכים קודם**`נתבייע` (5 תווים) לפני `עייד` (3 תווים) 3. **ראשי תיבות ארוכים קודם**`נתבייע` (5 תווים) לפני `עייד` (3 תווים)
4. **דווח מקומות מסופקים** — סמן `[?]` ותן לאדם להחליט 4. **דווח מקומות מסופקים** — סמן `[?]` ותן לאדם להחליט
5. **אל תמציא טקסט** — אם חסר משהו, סמן `[...]` ואל תנחש 5. **אל תמציא טקסט** — אם חסר משהו, סמן `[...]` ואל תנחש

View File

@@ -687,6 +687,16 @@ async def update_decision(decision_id: UUID, **fields) -> None:
# ── Chunks & Vectors ─────────────────────────────────────────────── # ── Chunks & Vectors ───────────────────────────────────────────────
async def delete_document_chunks(document_id: UUID) -> int:
"""Delete all chunks for a document (used before reprocessing)."""
pool = await get_pool()
async with pool.acquire() as conn:
result = await conn.execute(
"DELETE FROM document_chunks WHERE document_id = $1", document_id
)
return int(result.split()[-1]) # e.g. "DELETE 5" -> 5
async def store_chunks( async def store_chunks(
document_id: UUID, document_id: UUID,
case_id: UUID | None, case_id: UUID | None,

View File

@@ -19,7 +19,7 @@ logger = logging.getLogger(__name__)
_FILENAME_RULES: list[tuple[str, str, float]] = [ _FILENAME_RULES: list[tuple[str, str, float]] = [
# (regex pattern on filename, doc_type, confidence) # (regex pattern on filename, doc_type, confidence)
(r"כתב.ערר|כתב-ערר", "appeal", 1.0), (r"כתב.ערר|כתב-ערר", "appeal", 1.0),
(r"תשובה|תשובת|תגובת|השלמת.טיעון|בקשה.להשלמת", "response", 1.0), (r"תשובה|תשובת|תגובה|תגובת|השלמת.טיעון|בקשה.להשלמת|הודעת.עמדה", "response", 1.0),
(r"פרוטוקול", "protocol", 1.0), (r"פרוטוקול", "protocol", 1.0),
(r"החלטת?.ביניים|החלטה.לתיקון", "decision", 0.95), (r"החלטת?.ביניים|החלטה.לתיקון", "decision", 0.95),
(r"הוראות.תכנית|תכנית", "plan", 1.0), (r"הוראות.תכנית|תכנית", "plan", 1.0),

View File

@@ -3,6 +3,7 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
from pathlib import Path
from uuid import UUID from uuid import UUID
from legal_mcp.services import chunker, db, embeddings, extractor, references_extractor from legal_mcp.services import chunker, db, embeddings, extractor, references_extractor
@@ -37,6 +38,17 @@ async def process_document(document_id: UUID, case_id: UUID) -> dict:
page_count=page_count, page_count=page_count,
) )
# Save extracted text to documents/extracted/ directory
original_path = Path(doc["file_path"])
extracted_dir = original_path.parent.parent / "extracted"
extracted_dir.mkdir(parents=True, exist_ok=True)
txt_path = extracted_dir / (original_path.stem + ".txt")
try:
txt_path.write_text(text, encoding="utf-8")
logger.info("Saved extracted text to %s", txt_path)
except Exception as e:
logger.warning("Failed to save text file (non-fatal): %s", e)
# Step 1.5: Classify document — local rules first, Claude Code headless fallback # Step 1.5: Classify document — local rules first, Claude Code headless fallback
classification_result = {} classification_result = {}
try: try:

View File

@@ -62,7 +62,12 @@ async def case_create(
# Initialize git repo for the case # Initialize git repo for the case
case_dir = config.find_case_dir(case_number) case_dir = config.find_case_dir(case_number)
case_dir.mkdir(parents=True, exist_ok=True) case_dir.mkdir(parents=True, exist_ok=True)
(case_dir / "documents").mkdir(exist_ok=True) docs_dir = case_dir / "documents"
docs_dir.mkdir(exist_ok=True)
(docs_dir / "original").mkdir(exist_ok=True)
(docs_dir / "extracted").mkdir(exist_ok=True)
(docs_dir / "proofread").mkdir(exist_ok=True)
(docs_dir / "backup").mkdir(exist_ok=True)
(case_dir / "drafts").mkdir(exist_ok=True) (case_dir / "drafts").mkdir(exist_ok=True)
# Save case metadata # Save case metadata

37
scripts/auto-sync-cases.sh Executable file
View File

@@ -0,0 +1,37 @@
#!/bin/bash
# Auto-sync case repos to Gitea
# Runs via crontab every minute, commits and pushes any changes found.
CASES_DIR="/home/chaim/legal-ai/data/cases"
LOG="/home/chaim/legal-ai/data/.auto-sync.log"
GIT_ENV="GIT_AUTHOR_NAME=Ezer Mishpati GIT_AUTHOR_EMAIL=legal@local GIT_COMMITTER_NAME=Ezer Mishpati GIT_COMMITTER_EMAIL=legal@local GIT_TERMINAL_PROMPT=0"
for status_dir in "$CASES_DIR"/new "$CASES_DIR"/in-progress "$CASES_DIR"/completed; do
[ -d "$status_dir" ] || continue
for case_dir in "$status_dir"/*/; do
[ -d "$case_dir/.git" ] || continue
cd "$case_dir" || continue
# Check for any changes (modified, new, deleted)
changes=$(git status --porcelain 2>/dev/null)
[ -z "$changes" ] && continue
# Stage all changes
git add -A 2>/dev/null
# Build commit message from changed files
changed_files=$(git diff --cached --name-only 2>/dev/null | head -5)
count=$(git diff --cached --name-only 2>/dev/null | wc -l)
case_name=$(basename "$case_dir")
msg="סנכרון אוטומטי — ${count} קבצים שונו"
# Commit
env $GIT_ENV git commit -m "$msg" --quiet 2>/dev/null
if [ $? -eq 0 ]; then
# Push (non-blocking, ignore errors)
git push origin main --quiet 2>/dev/null
echo "$(date '+%Y-%m-%d %H:%M:%S') | $case_name | $count files synced" >> "$LOG"
fi
done
done

View File

@@ -1,8 +1,8 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""BiDi-safe box-drawing table renderer for mixed Hebrew/English terminal output. """BiDi-safe box-drawing table renderer for mixed Hebrew/English terminal output.
Uses LRM (Left-to-Right Mark, U+200E) before box-drawing characters to prevent Uses Unicode directional marks to prevent the BiDi algorithm from breaking
the BiDi algorithm from breaking table alignment when Hebrew text is present. table alignment when Hebrew text is present.
Usage as module: Usage as module:
from scripts.bidi_table import bidi_table from scripts.bidi_table import bidi_table
@@ -14,14 +14,25 @@ Usage from CLI:
from __future__ import annotations from __future__ import annotations
LRM = "\u200E" # Left-to-Right Mark — invisible, prevents BiDi reordering import re
LRM = "\u200E" # Left-to-Right Mark
RLM = "\u200F" # Right-to-Left Mark
LRE = "\u202A" # Left-to-Right Embedding
PDF = "\u202C" # Pop Directional Formatting
_HEB_RE = re.compile(r'[\u0590-\u05FF]')
def _has_hebrew(text: str) -> bool:
return bool(_HEB_RE.search(text))
def bidi_table(headers: list[str], rows: list[list[str]]) -> str: def bidi_table(headers: list[str], rows: list[list[str]]) -> str:
"""Render a box-drawing table safe for mixed RTL/LTR terminal display.""" """Render a box-drawing table safe for mixed RTL/LTR terminal display."""
ncols = len(headers) ncols = len(headers)
# Calculate column widths # Calculate column widths (visual length, not counting bidi marks)
col_widths = [len(h) for h in headers] col_widths = [len(h) for h in headers]
for row in rows: for row in rows:
for i, cell in enumerate(row[:ncols]): for i, cell in enumerate(row[:ncols]):
@@ -35,8 +46,10 @@ def bidi_table(headers: list[str], rows: list[list[str]]) -> str:
for i in range(ncols): for i in range(ncols):
cell = cells[i] if i < len(cells) else "" cell = cells[i] if i < len(cells) else ""
padded = cell + " " * max(0, col_widths[i] - len(cell)) padded = cell + " " * max(0, col_widths[i] - len(cell))
parts.append(" " + padded + " ") # Wrap each cell: LRE forces left-to-right context for the cell,
return LRM + "" + (LRM + "").join(parts) + LRM + "" # so box-drawing chars stay in place. PDF closes the embedding.
parts.append(LRE + " " + padded + " " + PDF)
return LRM + "" + ("").join(parts) + ""
lines = [hline("", "", "")] lines = [hline("", "", "")]
lines.append(dataline(headers)) lines.append(dataline(headers))

View File

@@ -84,6 +84,9 @@ async def create_project(
# Link issue to legal-ai case via plugin state # Link issue to legal-ai case via plugin state
await _link_case_to_issue(conn, issue_id, case_number) await _link_case_to_issue(conn, issue_id, case_number)
# Verify project creation and close the setup issue
await _verify_and_close_setup_issue(conn, project_id, issue_id, identifier, case_number)
return { return {
"id": project_id, "id": project_id,
"company_id": company_id, "company_id": company_id,
@@ -140,6 +143,70 @@ async def _link_case_to_issue(conn: asyncpg.Connection, issue_id: str, case_numb
logger.info("Linked issue %s to case %s via plugin state", issue_id, case_number) logger.info("Linked issue %s to case %s via plugin state", issue_id, case_number)
async def _verify_and_close_setup_issue(
conn: asyncpg.Connection,
project_id: str,
issue_id: str,
identifier: str,
case_number: str,
) -> None:
"""Verify the project was created correctly, then transition the setup issue to done."""
# Move to in_progress while verifying
await conn.execute(
"UPDATE issues SET status = 'in_progress', started_at = now() WHERE id = $1",
issue_id,
)
logger.info("%s: בביצוע — מאמת יצירת פרויקט", identifier)
# Verify: project exists, issue is linked, plugin state exists
checks = []
project = await conn.fetchrow("SELECT id, name FROM projects WHERE id = $1::uuid", project_id)
checks.append(("פרויקט נוצר", project is not None))
issue = await conn.fetchrow(
"SELECT id, project_id FROM issues WHERE id = $1 AND project_id = $2::uuid",
issue_id, project_id,
)
checks.append(("משימה משויכת לפרויקט", issue is not None))
plugin_link = await conn.fetchrow(
"SELECT value_json FROM plugin_state WHERE scope_id = $1 AND state_key = 'legal-case-number'",
issue_id,
)
checks.append(("קישור למערכת המשפטית", plugin_link is not None))
all_ok = all(ok for _, ok in checks)
report_lines = [f"{'' if ok else ''} {name}" for name, ok in checks]
report = "\n".join(report_lines)
if all_ok:
await conn.execute(
"UPDATE issues SET status = 'done', completed_at = now() WHERE id = $1",
issue_id,
)
# Document the verification in a comment
await conn.execute(
"""INSERT INTO issue_comments (id, company_id, issue_id, body)
VALUES ($1, (SELECT company_id FROM issues WHERE id = $2), $2,
$3)""",
str(uuid.uuid4()), issue_id,
f"## אימות יצירת פרויקט — ערר {case_number}\n\n{report}\n\nהפרויקט נוצר בהצלחה. משימה נסגרה אוטומטית.",
)
logger.info("%s: הושלם — פרויקט אומת ונסגר", identifier)
else:
# Leave in_progress with a warning comment
failed = [name for name, ok in checks if not ok]
await conn.execute(
"""INSERT INTO issue_comments (id, company_id, issue_id, body)
VALUES ($1, (SELECT company_id FROM issues WHERE id = $2), $2,
$3)""",
str(uuid.uuid4()), issue_id,
f"## אימות יצירת פרויקט — ערר {case_number}\n\n{report}\n\n⚠️ בדיקות שנכשלו: {', '.join(failed)}",
)
logger.warning("%s: אימות נכשל — %s", identifier, ", ".join(failed))
async def get_project_url(case_number: str) -> str | None: async def get_project_url(case_number: str) -> str | None:
"""Find existing Paperclip project for a case number.""" """Find existing Paperclip project for a case number."""
conn = await asyncpg.connect(PAPERCLIP_DB_URL) conn = await asyncpg.connect(PAPERCLIP_DB_URL)