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:
145
CLAUDE.md
145
CLAUDE.md
@@ -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`
|
||||||
|
|||||||
16
Dockerfile
16
Dockerfile
@@ -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
|
||||||
|
|||||||
232
web/app.py
232
web/app.py
@@ -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 ────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user