Add chair feedback system and content checklists for block-yod
Backend changes cherry-picked from ui-rewrite branch to enable feedback API endpoints for the Next.js staging UI. - chair_feedback DB table + API endpoints (GET/POST/PATCH) - Content checklists by appeal subtype injected into block-yod prompt - MCP tools for recording and listing chair feedback - Corpus analysis documentation (24 decisions) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
240
docs/corpus-analysis.md
Normal file
240
docs/corpus-analysis.md
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
# ניתוח שיטתי של קורפוס ההחלטות — מפת תוכן
|
||||||
|
|
||||||
|
> נוצר: 2026-04-12
|
||||||
|
> מקור: ניתוח 24 החלטות מתוך `/data/training/proofread/`
|
||||||
|
> מטרה: לחלץ דפוסי תוכן בפרק הדיון וההכרעה לפי סוג תיק
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. סקירה כללית של הקורפוס
|
||||||
|
|
||||||
|
### הרכב הקורפוס
|
||||||
|
| סוג | כמות | החלטות |
|
||||||
|
|-----|------|--------|
|
||||||
|
| רישוי ובנייה (1xxx) | 22 | כל ההחלטות מלבד גבאי ובית הכרם |
|
||||||
|
| תמ"א 38 | 1 | בית הכרם 1126+1141 |
|
||||||
|
| סמכות/סף בלבד | 1 | גבאי 1105 |
|
||||||
|
| **היטל השבחה (8xxx)** | **0** | **פער קריטי — אין אף החלטה בקורפוס** |
|
||||||
|
|
||||||
|
### תוצאות
|
||||||
|
| תוצאה | כמות | דוגמאות |
|
||||||
|
|-------|------|---------|
|
||||||
|
| דחייה | 12 | עמית, פרומר, זעיתר, בית שמש, אנשין (חניה), אהרן, שטרית, גבאי, ירושלים שקופה, לבנון, יפה |
|
||||||
|
| קבלה | 5 | טלי-אביב, הראל 1043+1054, הראל 1071+1077, מינץ, לוי |
|
||||||
|
| קבלה חלקית | 4 | אמיתי, בר-און, אנשין (1096), בית הכרם, אואקנין |
|
||||||
|
|
||||||
|
### אורך פרק הדיון
|
||||||
|
- טווח: 465 — 12,000 מילים
|
||||||
|
- ממוצע: ~5,000 מילים
|
||||||
|
- הקצר ביותר: גבאי (465, סמכות בלבד)
|
||||||
|
- הארוך ביותר: תורן/1015 (~11,000, שימוש חורג), מינץ/1071 (~12,000, סבב שני)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. נושאים שנמצאו בפרקי הדיון — מפת תוכן מלאה
|
||||||
|
|
||||||
|
### 2.1 נושאים תכנוניים (Planning Content)
|
||||||
|
|
||||||
|
| נושא | מופיע ב-X החלטות | עומק טיפוסי | דוגמאות בולטות |
|
||||||
|
|------|-----------------|-------------|----------------|
|
||||||
|
| **ניתוח הוראות תכנית** (ציטוט ישיר, פרשנות) | 18/24 | 3-15 סעיפים | פרומר (MI/200), לבנון (Hal/435), בית הכרם (10038, 16000) |
|
||||||
|
| **חניה** (חישוב, נספח תנועה, חלופות) | 8/24 | 5-15 סעיפים | אנשין-1096 (הרחבה), בית הכרם (הרחבה), אנשין-1109, לוי |
|
||||||
|
| **קווי בניין ומרווחים** | 7/24 | 3-10 סעיפים | אמיתי, אואקנין, שטרית, בר-און |
|
||||||
|
| **גובה/קומות** | 4/24 | 3-6 סעיפים | לבנון (הרחבה), בר-און, לוי |
|
||||||
|
| **סביבה ואופי שכונה** | 6/24 | 2-5 סעיפים | זעיתר (טיפולוגיה), פרומר (חקלאי), בית הכרם |
|
||||||
|
| **שימושים מותרים/שימוש חורג** | 2/24 | 5-20 סעיפים | תורן (הרחבה חריגה), יפה |
|
||||||
|
| **שימור** | 2/24 | 2-5 סעיפים | בית הכרם, בר-און (עצים) |
|
||||||
|
| **טופוגרפיה/טיפולוגיה** | 2/24 | 3-8 סעיפים | זעיתר (הרחבה), לבנון |
|
||||||
|
| **תכנית אב כמסגרת** | 2/24 | 2-3 סעיפים | בית הכרם (16000), תורן (צור הדסה) |
|
||||||
|
| **אינטרס ציבורי (חיזוק/התחדשות)** | 2/24 | 3-8 סעיפים | בית הכרם (תמ"א 38), מינץ |
|
||||||
|
| **היררכיית תכניות** (ארצית→מחוזית→מקומית) | 3/24 | 5-12 סעיפים | פרומר (הרחבה), לבנון, תורן |
|
||||||
|
| **נספח בינוי** (ניתוח פרטני) | 5/24 | 3-8 סעיפים | לבנון, לוי, מינץ, בר-און, בית שמש |
|
||||||
|
| **פגיעה בשכנים** (צל, פרטיות, רעש) | 5/24 | 2-5 סעיפים | אמיתי, שטרית, בית הכרם, אואקנין, זעיתר |
|
||||||
|
| **עצים/נוף** | 3/24 | 1-3 סעיפים | בר-און, בית שמש, בית הכרם |
|
||||||
|
|
||||||
|
### 2.2 נושאים משפטיים (Legal Content)
|
||||||
|
|
||||||
|
| נושא | מופיע ב-X החלטות | עומק טיפוסי |
|
||||||
|
|------|-----------------|-------------|
|
||||||
|
| **סמכות/זכות ערר** (ס' 152, 12ב) | 10/24 | 3-12 סעיפים |
|
||||||
|
| **הלכת שפר** (ערר על היתר תואם תכנית) | 8/24 | 2-5 סעיפים |
|
||||||
|
| **תימוכין קנייניים** (property feasibility) | 6/24 | 5-15 סעיפים |
|
||||||
|
| **סטייה ניכרת** (תקנה 2) | 5/24 | 3-8 סעיפים |
|
||||||
|
| **שיהוי** (delay/laches) | 5/24 | 2-5 סעיפים |
|
||||||
|
| **מידתיות** (proportionality) | 4/24 | 2-5 סעיפים |
|
||||||
|
| **עבריינות בנייה** (building violations) | 6/24 | 2-5 סעיפים |
|
||||||
|
| **שיקול דעת הוועדה המקומית** | 8/24 | 2-5 סעיפים |
|
||||||
|
| **קניין vs. תכנון** (הפרדת סמכויות) | 7/24 | 3-10 סעיפים |
|
||||||
|
| **הכשרת בנייה קיימת** (regularization) | 3/24 | 5-12 סעיפים |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. דפוסי "דיון תכנוני" שזוהו
|
||||||
|
|
||||||
|
### 3.1 מתי דפנה מקיימת דיון תכנוני מקיף?
|
||||||
|
|
||||||
|
**תמיד** כאשר:
|
||||||
|
- הערר עוסק בהתאמה לתכנית (ייעוד, שימוש, גובה, בנייה)
|
||||||
|
- יש שאלה של סטייה מהוראות תכנית
|
||||||
|
- הנושא הוא חניה/תשתיות
|
||||||
|
- התיק מערב תמ"א 38 או התחדשות עירונית
|
||||||
|
|
||||||
|
**לעולם לא** כאשר:
|
||||||
|
- התיק הוא סף/סמכות בלבד (גבאי)
|
||||||
|
- השאלה היא קניינית טהורה (טלי-אביב/1043+1054, בית שמש/1180+1181)
|
||||||
|
|
||||||
|
**עומק משתנה** כאשר:
|
||||||
|
- יש מספר נושאים שחלקם תכנוניים — הדיון התכנוני מוגבל לנושאים הרלוונטיים
|
||||||
|
|
||||||
|
### 3.2 איך דפנה בונה דיון תכנוני — הדפוס
|
||||||
|
|
||||||
|
**שלב 1: הקשר תכנוני רחב (2-8 סעיפים)**
|
||||||
|
- תכניות חלות ברמה הרלוונטית (מקומית, מחוזית, ארצית)
|
||||||
|
- ייעוד הקרקע, שימושים מותרים
|
||||||
|
- אופי הסביבה, מרקם בנוי
|
||||||
|
- *דוגמה*: פרומר — 12 סעיפים על MI/200, TAMA 35, TAMAM 30/1
|
||||||
|
|
||||||
|
**שלב 2: ציטוט ישיר מהוראות תכנית (3-15 סעיפים)**
|
||||||
|
- בלוקים ארוכים (200-600 מילים) של הוראות תכנית
|
||||||
|
- הדגשות בולד על המילים הרלוונטיות
|
||||||
|
- "הדגשת הח"מ" / "הדגשת הח.מ."
|
||||||
|
- *דוגמה*: בית הכרם — 400+ מילים מהוראות חניה של תכנית 5166ב
|
||||||
|
|
||||||
|
**שלב 3: יישום על המקרה הספציפי (3-8 סעיפים)**
|
||||||
|
- הוראה → עובדה → מסקנה
|
||||||
|
- "הנה מה שאומרת התכנית, הנה מה שקורה בפועל, הנה המסקנה"
|
||||||
|
- *דוגמה*: לבנון — השוואת חתכים של נספח בינוי עם הבקשה
|
||||||
|
|
||||||
|
**שלב 4: מסקנה תכנונית (1-3 סעיפים)**
|
||||||
|
- האם הבקשה תואמת/סוטה
|
||||||
|
- האם הסטייה מוצדקת
|
||||||
|
- מה צריך לתקן
|
||||||
|
|
||||||
|
### 3.3 הדפוס של "דיון תכנוני" לפי סוג נושא
|
||||||
|
|
||||||
|
| נושא | סדר ניתוח טיפוסי | רמת עומק |
|
||||||
|
|------|-----------------|----------|
|
||||||
|
| **חניה** | הוראות תכנית → תקנות חניה → נספח תנועה → חישוב → חלופות (קרן חניה, חפיפה, תחבורה ציבורית) | עמוק מאוד (8-15 סעיפים) |
|
||||||
|
| **קווי בניין** | הוראת תכנית → סטייה ניכרת? (תקנה 2(19)) → מידתיות → פגיעה בשכנים | בינוני-עמוק (5-10 סעיפים) |
|
||||||
|
| **גובה** | הוראת תכנית → נספח בינוי → מטרת ההגבלה → סטייה ניכרת? | בינוני (4-8 סעיפים) |
|
||||||
|
| **ייעוד/שימוש** | פרשנות תכנית → היררכיית תכניות → פרשנות מהותית → יישום | עמוק מאוד (10-20 סעיפים) |
|
||||||
|
| **שכנות** | עובדות (סיור) → השפעה (צל, פרטיות, רעש) → מידתיות | בינוני (3-6 סעיפים) |
|
||||||
|
| **סביבה** | תכנית אב → אופי שכונה → מרקם → השתלבות | בינוני (3-5 סעיפים) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. דפוסים חוצי-החלטות
|
||||||
|
|
||||||
|
### 4.1 מבנה הדיון — סדר הנושאים
|
||||||
|
1. **שאלות סף** (אם יש) — סמכות, זכות ערר, שיהוי
|
||||||
|
2. **הקשר תכנוני רחב** — תכניות, ייעוד, סביבה
|
||||||
|
3. **ניתוח ענייני** — נושא אחר נושא, כל אחד ב-CREAC
|
||||||
|
4. **מענה לטענות ספציפיות** — עובר על כל טענה מבלוק ז
|
||||||
|
5. **מסקנה** — תוצאה + הוראות אופרטיביות
|
||||||
|
|
||||||
|
### 4.2 כמה תכנון יש בכל החלטה?
|
||||||
|
|
||||||
|
| דרגה | תיאור | החלטות |
|
||||||
|
|------|-------|--------|
|
||||||
|
| **כבד** (>50% תכנון) | הדיון הוא בעיקר תכנוני | פרומר, זעיתר, בית הכרם, תורן, לבנון |
|
||||||
|
| **מאוזן** (30-50%) | שילוב תכנון + משפט | עמית, אמיתי, בר-און, אנשין-1096, אואקנין, לוי, שטרית |
|
||||||
|
| **קל** (<30%) | בעיקר משפטי, תכנון מינימלי | בית שמש, אנשין-1109, יפה |
|
||||||
|
| **אין** (0%) | רק משפטי/סמכות | טלי-אביב, הראל 1043+1054, גבאי, ירושלים שקופה |
|
||||||
|
|
||||||
|
### 4.3 פסיקה חוזרת (Recurring Case Law)
|
||||||
|
| פסיקה | נושא | מופיעה ב-X החלטות |
|
||||||
|
|-------|------|-------------------|
|
||||||
|
| הלכת שפר (עע"מ 317/10) | ערר על היתר תואם תכנית | 8 |
|
||||||
|
| הלכת עייזן (בג"ץ 1578/90) | תימוכין קנייניים | 6 |
|
||||||
|
| הלכת בן-יקר-גת | סטייה ניכרת | 4 |
|
||||||
|
| ערר אדלר 1181/22 | שיקול דעת תמ"א 38 | 2 |
|
||||||
|
| עע"מ 3975/22 קרן נכסים | קניין vs. תכנון | 4 |
|
||||||
|
|
||||||
|
### 4.4 טכניקות ניתוח ייחודיות
|
||||||
|
1. **פרשנות הרמונית** — כשיש מספר תכניות, דפנה מפרשת אותן ביחד (תורן)
|
||||||
|
2. **בדיקת תקדימים עובדתית** — הוועדה בדקה בעצמה 3 נכסים שנטענו כתקדים (לבנון)
|
||||||
|
3. **ציטוט מהחלטה מרכזת** — במקום לצטט 7 פס"ד, מצטטת אחד שריכז את כולם
|
||||||
|
4. **מבחן "המגרש הריק"** — להכשרת בנייה קיימת (אמיתי)
|
||||||
|
5. **מיפוי מתחים** — רשימת 3-6 מתחים לפני הניתוח (בית הכרם)
|
||||||
|
6. **"למעלה מן הצורך"** — דיון obiter אחרי הכרעה בסף (עמית, בית שמש)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. פערים שזוהו
|
||||||
|
|
||||||
|
### 5.1 פער קריטי: אין החלטות היטל השבחה בקורפוס
|
||||||
|
למרות שהמערכת מגדירה 3 סוגי עררים (רישוי, היטל השבחה, פיצויים) — **כל 24 ההחלטות הן רישוי ובנייה**. אין לנו אף מודל לכתיבת החלטה בהיטל השבחה.
|
||||||
|
|
||||||
|
### 5.2 פער: לא כל נושא תכנוני מכוסה
|
||||||
|
נושאים שמופיעים רק בהחלטה אחת-שתיים:
|
||||||
|
- שימור → רק בית הכרם
|
||||||
|
- תמ"א 38 → רק בית הכרם
|
||||||
|
- שימוש חורג → רק תורן
|
||||||
|
- טיפולוגיה/טופוגרפיה → רק זעיתר
|
||||||
|
- תכנית אב כמסגרת → רק בית הכרם + תורן
|
||||||
|
|
||||||
|
### 5.3 פער: הפרומפט הנוכחי לא מכיל "צ'קליסט תוכן"
|
||||||
|
הפרומפט של block-yod (שורות 198-234 ב-block_writer.py) אומר:
|
||||||
|
- ✅ CREAC methodology
|
||||||
|
- ✅ ענה על כל טענה
|
||||||
|
- ✅ צטט פסיקה
|
||||||
|
- ❌ **אין**: "בתיק רישוי, כסה את הנושאים התכנוניים הרלוונטיים"
|
||||||
|
- ❌ **אין**: צ'קליסט תוכן לפי סוג ערר
|
||||||
|
- ❌ **אין**: "הקשר תכנוני רחב" כמרכיב חובה
|
||||||
|
|
||||||
|
### 5.4 פער: הבחנה לא מספיקה בין תת-סוגי רישוי
|
||||||
|
תיקי רישוי שונים מאוד זה מזה:
|
||||||
|
- **סמכות/סף** — דיון משפטי טהור, אין צורך בתכנון
|
||||||
|
- **קנייני** — תימוכין קנייניים, אין צורך בתכנון
|
||||||
|
- **תכנוני מובהק** — ייעוד, חניה, גובה — דיון תכנוני מקיף
|
||||||
|
- **שימוש חורג** — פרשנות תכניות, דיון תכנוני עמוק
|
||||||
|
- **הקלה** — מידתיות + תכנון
|
||||||
|
- **תמ"א 38** — איזון אינטרסים + תכנון
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. המלצות לשלב הבא
|
||||||
|
|
||||||
|
### 6.1 צ'קליסט תוכן מוצע לערר רישוי ובנייה
|
||||||
|
```
|
||||||
|
בהתאם לנושא הערר, הדיון צריך לכלול:
|
||||||
|
|
||||||
|
□ הקשר תכנוני רחב (תמיד כשהערר מגיע למריט):
|
||||||
|
- תכניות חלות (מקומית, מחוזית, ארצית — לפי הצורך)
|
||||||
|
- ייעוד הקרקע
|
||||||
|
- אופי הסביבה
|
||||||
|
|
||||||
|
□ ניתוח הוראות תכנית (כשיש שאלה של התאמה/סטייה):
|
||||||
|
- ציטוט ישיר מהוראות רלוונטיות
|
||||||
|
- פרשנות — תכלית ההוראה
|
||||||
|
- יישום על המקרה
|
||||||
|
|
||||||
|
□ חניה (כשרלוונטי):
|
||||||
|
- הוראות תכנית + נספח תנועה
|
||||||
|
- חישוב מקומות נדרשים vs. מסופקים
|
||||||
|
- חלופות (קרן חניה, חפיפה, תח"צ)
|
||||||
|
|
||||||
|
□ שכנות/פגיעה (כשרלוונטי):
|
||||||
|
- ממצאי סיור
|
||||||
|
- צל, פרטיות, רעש, נוף
|
||||||
|
- מידתיות
|
||||||
|
|
||||||
|
□ קווי בניין (כשרלוונטי):
|
||||||
|
- הוראת תכנית
|
||||||
|
- סטייה ניכרת — תקנה 2(19)
|
||||||
|
- הצדקה/מידתיות
|
||||||
|
|
||||||
|
□ גובה/קומות (כשרלוונטי):
|
||||||
|
- הוראת תכנית + נספח בינוי
|
||||||
|
- מטרת ההגבלה
|
||||||
|
- סטייה ניכרת — תקנה 2(10)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 הבחנה בין תת-סוגים
|
||||||
|
הפרומפט צריך לזהות את סוג הערר ולהתאים את הצ'קליסט:
|
||||||
|
- **ערר סמכות/סף** → ללא דיון תכנוני
|
||||||
|
- **ערר קנייני** → דיון משפטי, ללא תכנון
|
||||||
|
- **ערר מהותי** → דיון תכנוני מקיף + משפטי
|
||||||
|
|
||||||
|
### 6.3 צורך דחוף: החלטות היטל השבחה
|
||||||
|
צריך להוסיף לקורפוס לפחות 5-10 החלטות של היטל השבחה לפני שהמערכת יכולה לכתוב החלטות בתחום הזה.
|
||||||
@@ -390,6 +390,29 @@ async def ingest_final_version(
|
|||||||
return await workflow.ingest_final_version(case_number, file_path, final_text)
|
return await workflow.ingest_final_version(case_number, file_path, final_text)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def record_chair_feedback(
|
||||||
|
case_number: str,
|
||||||
|
feedback_text: str,
|
||||||
|
block_id: str = "block-yod",
|
||||||
|
category: str = "missing_content",
|
||||||
|
lesson_extracted: str = "",
|
||||||
|
) -> str:
|
||||||
|
"""תיעוד הערת יו"ר (דפנה) על טיוטת החלטה — חסר, שגיאה, סגנון."""
|
||||||
|
return await workflow.record_chair_feedback(
|
||||||
|
case_number, feedback_text, block_id, category, lesson_extracted,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def list_chair_feedback(
|
||||||
|
case_number: str = "",
|
||||||
|
category: str = "",
|
||||||
|
unresolved_only: bool = True,
|
||||||
|
) -> str:
|
||||||
|
"""הצגת הערות יו"ר שתועדו — אפשר לסנן לפי תיק, קטגוריה, מטופלות."""
|
||||||
|
return await workflow.list_chair_feedback(case_number, category, unresolved_only)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
mcp.run(transport="stdio")
|
mcp.run(transport="stdio")
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ from uuid import UUID
|
|||||||
|
|
||||||
from legal_mcp import config
|
from legal_mcp import config
|
||||||
from legal_mcp.services import db, embeddings, claude_session
|
from legal_mcp.services import db, embeddings, claude_session
|
||||||
|
from legal_mcp.services.lessons import get_content_checklist
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -215,6 +216,8 @@ BLOCK_PROMPTS = {
|
|||||||
- **ללא כותרות משנה** (חריג: נושאים נפרדים לחלוטין)
|
- **ללא כותרות משנה** (חריג: נושאים נפרדים לחלוטין)
|
||||||
- מספור רציף
|
- מספור רציף
|
||||||
|
|
||||||
|
{content_checklist}
|
||||||
|
|
||||||
## כיוון מאושר (חובה):
|
## כיוון מאושר (חובה):
|
||||||
{direction_context}
|
{direction_context}
|
||||||
|
|
||||||
@@ -310,6 +313,15 @@ async def write_block(
|
|||||||
outcome = (decision or {}).get("outcome", "rejected")
|
outcome = (decision or {}).get("outcome", "rejected")
|
||||||
structure_guidance = STRUCTURE_GUIDANCE.get(outcome, "")
|
structure_guidance = STRUCTURE_GUIDANCE.get(outcome, "")
|
||||||
|
|
||||||
|
# Content checklist — tells block-yod WHAT topics to cover
|
||||||
|
content_checklist = ""
|
||||||
|
if block_id == "block-yod":
|
||||||
|
content_checklist = get_content_checklist(
|
||||||
|
appeal_type=case.get("appeal_type", ""),
|
||||||
|
subject=case.get("subject", ""),
|
||||||
|
subject_categories=case.get("subject_categories", []),
|
||||||
|
)
|
||||||
|
|
||||||
# Format prompt — per Anthropic long-context best practices:
|
# Format prompt — per Anthropic long-context best practices:
|
||||||
# Place source documents FIRST (top of prompt), instructions LAST.
|
# Place source documents FIRST (top of prompt), instructions LAST.
|
||||||
# "Queries at the end can improve response quality by up to 30%"
|
# "Queries at the end can improve response quality by up to 30%"
|
||||||
@@ -323,6 +335,7 @@ async def write_block(
|
|||||||
style_context=style_context,
|
style_context=style_context,
|
||||||
discussion_context=discussion_context,
|
discussion_context=discussion_context,
|
||||||
structure_guidance=structure_guidance,
|
structure_guidance=structure_guidance,
|
||||||
|
content_checklist=content_checklist,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Restructure: sources first, then instructions
|
# Restructure: sources first, then instructions
|
||||||
|
|||||||
@@ -462,6 +462,22 @@ CREATE TABLE IF NOT EXISTS case_law_embeddings (
|
|||||||
created_at TIMESTAMPTZ DEFAULT now()
|
created_at TIMESTAMPTZ DEFAULT now()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
-- ═══════════════════════════════════════════════════════════════════
|
||||||
|
-- Chair Feedback (הערות דפנה על טיוטות)
|
||||||
|
-- ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS chair_feedback (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
case_id UUID REFERENCES cases(id) ON DELETE SET NULL,
|
||||||
|
block_id TEXT DEFAULT '', -- block-yod, block-vav, etc.
|
||||||
|
feedback_text TEXT NOT NULL, -- ההערה של דפנה
|
||||||
|
category TEXT DEFAULT 'other', -- missing_content/wrong_tone/wrong_structure/factual_error/style/other
|
||||||
|
lesson_extracted TEXT DEFAULT '', -- הלקח שהופק
|
||||||
|
applied_to TEXT[] DEFAULT '{}', -- לאילו קבצים/כללים הלקח יושם
|
||||||
|
resolved BOOLEAN DEFAULT FALSE, -- האם הלקח יושם
|
||||||
|
created_at TIMESTAMPTZ DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
-- ═══════════════════════════════════════════════════════════════════
|
-- ═══════════════════════════════════════════════════════════════════
|
||||||
-- Indexes
|
-- Indexes
|
||||||
-- ═══════════════════════════════════════════════════════════════════
|
-- ═══════════════════════════════════════════════════════════════════
|
||||||
@@ -1321,3 +1337,72 @@ async def search_precedents(
|
|||||||
|
|
||||||
results.sort(key=lambda x: x["score"], reverse=True)
|
results.sort(key=lambda x: x["score"], reverse=True)
|
||||||
return results[:limit]
|
return results[:limit]
|
||||||
|
|
||||||
|
|
||||||
|
# ── Chair feedback ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async def record_chair_feedback(
|
||||||
|
case_id: UUID | None,
|
||||||
|
block_id: str,
|
||||||
|
feedback_text: str,
|
||||||
|
category: str = "other",
|
||||||
|
lesson_extracted: str = "",
|
||||||
|
) -> UUID:
|
||||||
|
"""Record feedback from the chair (Dafna) on a draft block."""
|
||||||
|
pool = await get_pool()
|
||||||
|
feedback_id = uuid4()
|
||||||
|
async with pool.acquire() as conn:
|
||||||
|
await conn.execute(
|
||||||
|
"""INSERT INTO chair_feedback
|
||||||
|
(id, case_id, block_id, feedback_text, category, lesson_extracted)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6)""",
|
||||||
|
feedback_id, case_id, block_id, feedback_text, category,
|
||||||
|
lesson_extracted,
|
||||||
|
)
|
||||||
|
return feedback_id
|
||||||
|
|
||||||
|
|
||||||
|
async def list_chair_feedback(
|
||||||
|
case_id: UUID | None = None,
|
||||||
|
category: str | None = None,
|
||||||
|
unresolved_only: bool = False,
|
||||||
|
) -> list[dict]:
|
||||||
|
"""List chair feedback, optionally filtered."""
|
||||||
|
pool = await get_pool()
|
||||||
|
conditions = []
|
||||||
|
params: list = []
|
||||||
|
idx = 1
|
||||||
|
|
||||||
|
if case_id:
|
||||||
|
conditions.append(f"case_id = ${idx}")
|
||||||
|
params.append(case_id)
|
||||||
|
idx += 1
|
||||||
|
if category:
|
||||||
|
conditions.append(f"category = ${idx}")
|
||||||
|
params.append(category)
|
||||||
|
idx += 1
|
||||||
|
if unresolved_only:
|
||||||
|
conditions.append("resolved = FALSE")
|
||||||
|
|
||||||
|
where = f"WHERE {' AND '.join(conditions)}" if conditions else ""
|
||||||
|
async with pool.acquire() as conn:
|
||||||
|
rows = await conn.fetch(
|
||||||
|
f"SELECT * FROM chair_feedback {where} ORDER BY created_at DESC",
|
||||||
|
*params,
|
||||||
|
)
|
||||||
|
return [dict(r) for r in rows]
|
||||||
|
|
||||||
|
|
||||||
|
async def resolve_chair_feedback(
|
||||||
|
feedback_id: UUID,
|
||||||
|
applied_to: list[str],
|
||||||
|
) -> None:
|
||||||
|
"""Mark feedback as resolved and record where it was applied."""
|
||||||
|
pool = await get_pool()
|
||||||
|
async with pool.acquire() as conn:
|
||||||
|
await conn.execute(
|
||||||
|
"""UPDATE chair_feedback
|
||||||
|
SET resolved = TRUE, applied_to = $2
|
||||||
|
WHERE id = $1""",
|
||||||
|
feedback_id, applied_to,
|
||||||
|
)
|
||||||
|
|||||||
@@ -329,3 +329,193 @@ def format_ratios_comment(outcome: str, section: str) -> str:
|
|||||||
lo, hi = ratios[section]
|
lo, hi = ratios[section]
|
||||||
return f"יעד: {lo}-{hi}% מסך ההחלטה"
|
return f"יעד: {lo}-{hi}% מסך ההחלטה"
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
# ── Content checklists by appeal subtype ──────────────────────────
|
||||||
|
# Based on systematic analysis of 24 decisions from Dafna's corpus.
|
||||||
|
# See: docs/corpus-analysis.md
|
||||||
|
|
||||||
|
CONTENT_CHECKLISTS: dict[str, str] = {
|
||||||
|
"licensing_substantive": """## צ'קליסט תוכן — ערר רישוי מהותי (חובה)
|
||||||
|
הדיון חייב לכלול את הנושאים הרלוונטיים מהרשימה הבאה.
|
||||||
|
**אל תדלג על נושא שרלוונטי לתיק — בדוק כל סעיף.**
|
||||||
|
|
||||||
|
### א. הקשר תכנוני רחב (חובה בכל ערר מהותי)
|
||||||
|
- תכניות חלות — ציין את התכניות הרלוונטיות ברמה מקומית, מחוזית וארצית (לפי הצורך)
|
||||||
|
- ייעוד הקרקע — מה הייעוד בתכנית? מה השימושים המותרים?
|
||||||
|
- אופי הסביבה — מרקם בנוי, צפיפות, אופי שכונה/ישוב
|
||||||
|
- *דוגמה*: בערר פרומר — 12 סעיפים על MI/200, תמ"א 35, תמ"מ 30/1
|
||||||
|
|
||||||
|
### ב. ניתוח הוראות תכנית (כשיש שאלה של התאמה/סטייה)
|
||||||
|
- ציטוט ישיר מהוראות התכנית הרלוונטיות (200-600 מילים לכל ציטוט)
|
||||||
|
- פרשנות — מה תכלית ההוראה?
|
||||||
|
- יישום — האם הבקשה תואמת או סוטה?
|
||||||
|
- *דוגמה*: בערר לבנון — ניתוח חתכים של נספח בינוי מול הבקשה
|
||||||
|
|
||||||
|
### ג. חניה (כשרלוונטי — מופיע ב-8 מתוך 24 החלטות)
|
||||||
|
- הוראות תכנית + נספח תנועה (ציטוט ישיר)
|
||||||
|
- חישוב מקומות חניה נדרשים vs. מסופקים
|
||||||
|
- חלופות: קרן חניה, חפיפת שימושים, קרבה לתח"צ
|
||||||
|
- *דוגמה*: בערר בית הכרם — 8 סעיפים, 400+ מילים מהוראות תכנית 5166ב
|
||||||
|
|
||||||
|
### ד. קווי בניין ומרווחים (כשרלוונטי)
|
||||||
|
- הוראת תכנית על מרווחים
|
||||||
|
- סטייה ניכרת? — תקנה 2(19) / הלכת בן-יקר-גת
|
||||||
|
- הצדקה + מידתיות — פגיעה בשכנים?
|
||||||
|
|
||||||
|
### ה. גובה וקומות (כשרלוונטי)
|
||||||
|
- הוראת תכנית + נספח בינוי (חתכים)
|
||||||
|
- מטרת ההגבלה — למה יש הגבלת גובה כאן?
|
||||||
|
- סטייה ניכרת — תקנה 2(10) / 2(8)
|
||||||
|
|
||||||
|
### ו. פגיעה בשכנים (כשרלוונטי)
|
||||||
|
- ממצאי סיור באתר
|
||||||
|
- השפעה: צל, פרטיות, רעש, נוף
|
||||||
|
- מידתיות — האם הפגיעה סבירה?
|
||||||
|
|
||||||
|
### ז. שימוש חורג (כשרלוונטי)
|
||||||
|
- מה השימוש המותר בתכנית? מה השימוש המבוקש?
|
||||||
|
- "מבחן ההתאמה" — האם השימוש מתאים למיקום?
|
||||||
|
- תנאים ומגבלות
|
||||||
|
""",
|
||||||
|
|
||||||
|
"licensing_threshold": """## צ'קליסט תוכן — ערר רישוי סף/סמכות
|
||||||
|
הערר עוסק בשאלות סף — אין צורך בדיון תכנוני מקיף.
|
||||||
|
|
||||||
|
### א. שאלת הסמכות
|
||||||
|
- סעיפי חוק רלוונטיים (ס' 12ב, 152, וכו')
|
||||||
|
- פסיקה על גבולות הסמכות
|
||||||
|
|
||||||
|
### ב. זכות ערר
|
||||||
|
- מי רשאי לערור? באיזה מסלול?
|
||||||
|
- הלכת שפר (עע"מ 317/10) — כשרלוונטית
|
||||||
|
|
||||||
|
### ג. שיהוי (אם רלוונטי)
|
||||||
|
""",
|
||||||
|
|
||||||
|
"licensing_property": """## צ'קליסט תוכן — ערר רישוי קנייני
|
||||||
|
הערר עוסק בעיקר בשאלת תימוכין קנייניים — דיון משפטי.
|
||||||
|
|
||||||
|
### א. מסגרת נורמטיבית
|
||||||
|
- הלכת עייזן, בני אליעזר, רוזן — "היתכנות קניינית"
|
||||||
|
- ס' 71ב לחוק המקרקעין
|
||||||
|
|
||||||
|
### ב. בחינת הראיות
|
||||||
|
- הסכמות, רישום, היסטוריית בנייה
|
||||||
|
- חלוקה דה-פקטו ארוכת שנים
|
||||||
|
|
||||||
|
### ג. הפרדה בין קניין לתכנון
|
||||||
|
- גוף תכנוני אינו מכריע בסכסוכי קניין
|
||||||
|
- "היתכנות קניינית" ≠ הוכחת בעלות
|
||||||
|
|
||||||
|
### ד. שאלות תכנוניות (אם רלוונטיות)
|
||||||
|
- אם הערר עולה גם שאלות תכנוניות — דון בהן בנפרד
|
||||||
|
""",
|
||||||
|
|
||||||
|
"tama38": """## צ'קליסט תוכן — ערר תמ"א 38
|
||||||
|
הדיון חייב לאזן בין אינטרס ציבורי לפגיעה בשכנים.
|
||||||
|
|
||||||
|
### א. אינטרס ציבורי — חיזוק/התחדשות
|
||||||
|
- עוצמת האינטרס — בניין גדול vs. בית בודד
|
||||||
|
- "בית בודד" מחליש את אינטרס החיזוק
|
||||||
|
- תרומה לרקמה העירונית
|
||||||
|
|
||||||
|
### ב. תכנית אב / מדיניות אזורית
|
||||||
|
- האם יש תכנית אב? מדיניות 16000?
|
||||||
|
- התאמה לראיה כללית vs. אד-הוק
|
||||||
|
|
||||||
|
### ג. ניתוח השוואתי
|
||||||
|
- זכויות לפי תכנית קיימת vs. מבוקש לפי תמ"א 38
|
||||||
|
- שטחים, קומות, קווי בניין — טבלת השוואה
|
||||||
|
|
||||||
|
### ד. שימור (כשרלוונטי)
|
||||||
|
- חוות דעת אגף שימור
|
||||||
|
- השפעה על מיקום/צורת הבניין
|
||||||
|
|
||||||
|
### ה. חניה (כמעט תמיד רלוונטי)
|
||||||
|
- הוראות תכנית + ס' 17 לתמ"א 38
|
||||||
|
- פטורים — קרבה לתח"צ, קרן חניה, תכנית אב
|
||||||
|
- ניתוח מפורט של חלופות
|
||||||
|
|
||||||
|
### ו. פגיעה בשכנים
|
||||||
|
- ממצאי סיור
|
||||||
|
- צל, פרטיות, קרבה
|
||||||
|
- מידתיות — מה הפגיעה ביחס לתועלת?
|
||||||
|
|
||||||
|
### ז. מטרדי בנייה
|
||||||
|
- "מטרד בנייה אינו עילה לסירוב" — אך תנאים נדרשים
|
||||||
|
- תכנית ארגון אתר
|
||||||
|
""",
|
||||||
|
|
||||||
|
"betterment_levy": """## צ'קליסט תוכן — ערר היטל השבחה
|
||||||
|
⚠️ שים לב: אין עדיין החלטות היטל השבחה בקורפוס האימון.
|
||||||
|
הצ'קליסט הזה מבוסס על ידע כללי — לא על ניתוח ספציפי של סגנון דפנה.
|
||||||
|
|
||||||
|
### א. המסגרת הנורמטיבית
|
||||||
|
- התוספת השלישית לחוק התכנון והבנייה
|
||||||
|
- אירוע מס — מה יצר את ההשבחה?
|
||||||
|
|
||||||
|
### ב. שומה
|
||||||
|
- שיטת השומה (שומה מכרעת / שמאי מייעץ)
|
||||||
|
- מועד הקובע
|
||||||
|
- זכויות בנייה — לפני ואחרי
|
||||||
|
|
||||||
|
### ג. שאלות משפטיות
|
||||||
|
- פטורים (ס' 19)
|
||||||
|
- מועדי תשלום
|
||||||
|
- שיערוך
|
||||||
|
|
||||||
|
### ד. ניתוח שמאי
|
||||||
|
- האם השומה תקינה?
|
||||||
|
- פערים בין השומות
|
||||||
|
""",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_content_checklist(
|
||||||
|
appeal_type: str = "",
|
||||||
|
subject: str = "",
|
||||||
|
subject_categories: list[str] | None = None,
|
||||||
|
) -> str:
|
||||||
|
"""Return the appropriate content checklist based on case characteristics.
|
||||||
|
|
||||||
|
Determines the subtype from case metadata:
|
||||||
|
- TAMA 38 cases → tama38 checklist
|
||||||
|
- Betterment levy (8xxx) → betterment_levy checklist
|
||||||
|
- Property-only cases → licensing_property checklist
|
||||||
|
- Threshold/jurisdiction cases → licensing_threshold checklist
|
||||||
|
- All other licensing → licensing_substantive checklist
|
||||||
|
"""
|
||||||
|
cats = subject_categories or []
|
||||||
|
subject_lower = subject.lower() if subject else ""
|
||||||
|
appeal_lower = appeal_type.lower() if appeal_type else ""
|
||||||
|
|
||||||
|
# TAMA 38
|
||||||
|
if any(
|
||||||
|
kw in subject_lower
|
||||||
|
for kw in ["תמ\"א 38", "תמא 38", "תמ\"א38", "חיזוק", "tama"]
|
||||||
|
) or "תמ\"א 38" in cats:
|
||||||
|
return CONTENT_CHECKLISTS["tama38"]
|
||||||
|
|
||||||
|
# Betterment levy
|
||||||
|
if "היטל השבחה" in appeal_lower or "betterment" in appeal_lower or any(
|
||||||
|
"היטל" in c for c in cats
|
||||||
|
):
|
||||||
|
return CONTENT_CHECKLISTS["betterment_levy"]
|
||||||
|
|
||||||
|
# Property-focused (תימוכין קנייניים)
|
||||||
|
if any(
|
||||||
|
kw in subject_lower
|
||||||
|
for kw in ["תימוכין", "קנייני", "בעלות", "הסכמת דיירים"]
|
||||||
|
):
|
||||||
|
return CONTENT_CHECKLISTS["licensing_property"]
|
||||||
|
|
||||||
|
# Threshold/jurisdiction
|
||||||
|
if any(
|
||||||
|
kw in subject_lower
|
||||||
|
for kw in ["סמכות", "סף", "סילוק על הסף", "זכות ערר"]
|
||||||
|
):
|
||||||
|
return CONTENT_CHECKLISTS["licensing_threshold"]
|
||||||
|
|
||||||
|
# Default: substantive licensing
|
||||||
|
return CONTENT_CHECKLISTS["licensing_substantive"]
|
||||||
|
|||||||
@@ -318,3 +318,97 @@ async def ingest_final_version(
|
|||||||
return json.dumps(result, default=str, ensure_ascii=False, indent=2)
|
return json.dumps(result, default=str, ensure_ascii=False, indent=2)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return json.dumps({"status": "error", "message": str(e)}, ensure_ascii=False, indent=2)
|
return json.dumps({"status": "error", "message": str(e)}, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Chair feedback tools ──────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
async def record_chair_feedback(
|
||||||
|
case_number: str,
|
||||||
|
feedback_text: str,
|
||||||
|
block_id: str = "block-yod",
|
||||||
|
category: str = "missing_content",
|
||||||
|
lesson_extracted: str = "",
|
||||||
|
) -> str:
|
||||||
|
"""תיעוד הערת יו"ר (דפנה) על טיוטת החלטה.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
case_number: מספר תיק הערר
|
||||||
|
feedback_text: ההערה של דפנה (מה חסר, מה לא נכון, מה צריך לשנות)
|
||||||
|
block_id: הבלוק שההערה מתייחסת אליו (ברירת מחדל: block-yod)
|
||||||
|
category: קטגוריה — missing_content/wrong_tone/wrong_structure/factual_error/style/other
|
||||||
|
lesson_extracted: הלקח שהופק מההערה (אם ברור כבר)
|
||||||
|
"""
|
||||||
|
case = await db.get_case_by_number(case_number)
|
||||||
|
case_id = UUID(case["id"]) if case else None
|
||||||
|
|
||||||
|
valid_categories = [
|
||||||
|
"missing_content", "wrong_tone", "wrong_structure",
|
||||||
|
"factual_error", "style", "other",
|
||||||
|
]
|
||||||
|
if category not in valid_categories:
|
||||||
|
return f"קטגוריה לא חוקית. אפשרויות: {', '.join(valid_categories)}"
|
||||||
|
|
||||||
|
feedback_id = await db.record_chair_feedback(
|
||||||
|
case_id=case_id,
|
||||||
|
block_id=block_id,
|
||||||
|
feedback_text=feedback_text,
|
||||||
|
category=category,
|
||||||
|
lesson_extracted=lesson_extracted,
|
||||||
|
)
|
||||||
|
|
||||||
|
return json.dumps({
|
||||||
|
"status": "ok",
|
||||||
|
"feedback_id": str(feedback_id),
|
||||||
|
"message": f"הערה נרשמה בהצלחה. קטגוריה: {category}.",
|
||||||
|
"next_steps": [
|
||||||
|
"כדי להפיק לקח מההערה, הפעל: analyze_chair_feedback",
|
||||||
|
"כדי לסמן כמטופל: resolve_chair_feedback",
|
||||||
|
],
|
||||||
|
}, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
async def list_chair_feedback(
|
||||||
|
case_number: str = "",
|
||||||
|
category: str = "",
|
||||||
|
unresolved_only: bool = True,
|
||||||
|
) -> str:
|
||||||
|
"""הצגת הערות יו"ר שתועדו, עם אפשרות סינון.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
case_number: סינון לפי תיק (אם ריק — כל ההערות)
|
||||||
|
category: סינון לפי קטגוריה
|
||||||
|
unresolved_only: האם להציג רק הערות שלא טופלו (ברירת מחדל: כן)
|
||||||
|
"""
|
||||||
|
case_id = None
|
||||||
|
if case_number:
|
||||||
|
case = await db.get_case_by_number(case_number)
|
||||||
|
if case:
|
||||||
|
case_id = UUID(case["id"])
|
||||||
|
|
||||||
|
feedbacks = await db.list_chair_feedback(
|
||||||
|
case_id=case_id,
|
||||||
|
category=category or None,
|
||||||
|
unresolved_only=unresolved_only,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not feedbacks:
|
||||||
|
return "אין הערות שמתאימות לסינון."
|
||||||
|
|
||||||
|
items = []
|
||||||
|
for fb in feedbacks:
|
||||||
|
items.append({
|
||||||
|
"id": str(fb["id"]),
|
||||||
|
"case_id": str(fb["case_id"]) if fb["case_id"] else None,
|
||||||
|
"block_id": fb["block_id"],
|
||||||
|
"category": fb["category"],
|
||||||
|
"feedback": fb["feedback_text"],
|
||||||
|
"lesson": fb["lesson_extracted"],
|
||||||
|
"resolved": fb["resolved"],
|
||||||
|
"date": fb["created_at"].isoformat() if fb.get("created_at") else None,
|
||||||
|
})
|
||||||
|
|
||||||
|
return json.dumps({
|
||||||
|
"total": len(items),
|
||||||
|
"feedbacks": items,
|
||||||
|
}, ensure_ascii=False, indent=2, default=str)
|
||||||
|
|||||||
127
web/app.py
127
web/app.py
@@ -2436,6 +2436,133 @@ async def api_reprocess_document(case_number: str, doc_id: str):
|
|||||||
return {"status": "reprocessing"}
|
return {"status": "reprocessing"}
|
||||||
|
|
||||||
|
|
||||||
|
# ── Chair feedback endpoints ──────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/feedback")
|
||||||
|
async def api_list_feedback(
|
||||||
|
case_number: str = "",
|
||||||
|
category: str = "",
|
||||||
|
unresolved_only: bool = False,
|
||||||
|
):
|
||||||
|
"""List chair feedback, optionally filtered by case/category."""
|
||||||
|
case_id = None
|
||||||
|
if case_number:
|
||||||
|
case = await db.get_case_by_number(case_number)
|
||||||
|
if case:
|
||||||
|
case_id = UUID(case["id"])
|
||||||
|
|
||||||
|
feedbacks = await db.list_chair_feedback(
|
||||||
|
case_id=case_id,
|
||||||
|
category=category or None,
|
||||||
|
unresolved_only=unresolved_only,
|
||||||
|
)
|
||||||
|
|
||||||
|
items = []
|
||||||
|
# Build case_number lookup
|
||||||
|
case_numbers: dict[str, str] = {}
|
||||||
|
pool = await db.get_pool()
|
||||||
|
for fb in feedbacks:
|
||||||
|
cid = fb.get("case_id")
|
||||||
|
cn = ""
|
||||||
|
if cid and str(cid) not in case_numbers:
|
||||||
|
async with pool.acquire() as conn:
|
||||||
|
row = await conn.fetchrow(
|
||||||
|
"SELECT case_number, title FROM cases WHERE id = $1", cid,
|
||||||
|
)
|
||||||
|
if row:
|
||||||
|
case_numbers[str(cid)] = row["case_number"]
|
||||||
|
if cid:
|
||||||
|
cn = case_numbers.get(str(cid), "")
|
||||||
|
|
||||||
|
items.append({
|
||||||
|
"id": str(fb["id"]),
|
||||||
|
"case_id": str(fb["case_id"]) if fb["case_id"] else None,
|
||||||
|
"case_number": cn,
|
||||||
|
"block_id": fb["block_id"],
|
||||||
|
"category": fb["category"],
|
||||||
|
"feedback_text": fb["feedback_text"],
|
||||||
|
"lesson_extracted": fb["lesson_extracted"],
|
||||||
|
"resolved": fb["resolved"],
|
||||||
|
"applied_to": fb.get("applied_to", []),
|
||||||
|
"created_at": fb["created_at"].isoformat() if fb.get("created_at") else None,
|
||||||
|
})
|
||||||
|
|
||||||
|
return items
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/feedback")
|
||||||
|
async def api_create_feedback(
|
||||||
|
case_number: str = Form(""),
|
||||||
|
block_id: str = Form("block-yod"),
|
||||||
|
feedback_text: str = Form(...),
|
||||||
|
category: str = Form("missing_content"),
|
||||||
|
lesson_extracted: str = Form(""),
|
||||||
|
):
|
||||||
|
"""Record a new chair feedback entry."""
|
||||||
|
case_id = None
|
||||||
|
if case_number:
|
||||||
|
case = await db.get_case_by_number(case_number)
|
||||||
|
if case:
|
||||||
|
case_id = UUID(case["id"])
|
||||||
|
|
||||||
|
valid_categories = [
|
||||||
|
"missing_content", "wrong_tone", "wrong_structure",
|
||||||
|
"factual_error", "style", "other",
|
||||||
|
]
|
||||||
|
if category not in valid_categories:
|
||||||
|
raise HTTPException(400, f"קטגוריה לא חוקית. אפשרויות: {', '.join(valid_categories)}")
|
||||||
|
|
||||||
|
feedback_id = await db.record_chair_feedback(
|
||||||
|
case_id=case_id,
|
||||||
|
block_id=block_id,
|
||||||
|
feedback_text=feedback_text,
|
||||||
|
category=category,
|
||||||
|
lesson_extracted=lesson_extracted,
|
||||||
|
)
|
||||||
|
|
||||||
|
return {"id": str(feedback_id), "status": "created"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/feedback/json")
|
||||||
|
async def api_create_feedback_json(body: dict):
|
||||||
|
"""Record a new chair feedback entry (JSON body)."""
|
||||||
|
case_number = body.get("case_number", "")
|
||||||
|
case_id = None
|
||||||
|
if case_number:
|
||||||
|
case = await db.get_case_by_number(case_number)
|
||||||
|
if case:
|
||||||
|
case_id = UUID(case["id"])
|
||||||
|
|
||||||
|
valid_categories = [
|
||||||
|
"missing_content", "wrong_tone", "wrong_structure",
|
||||||
|
"factual_error", "style", "other",
|
||||||
|
]
|
||||||
|
category = body.get("category", "missing_content")
|
||||||
|
if category not in valid_categories:
|
||||||
|
raise HTTPException(400, f"קטגוריה לא חוקית. אפשרויות: {', '.join(valid_categories)}")
|
||||||
|
|
||||||
|
feedback_id = await db.record_chair_feedback(
|
||||||
|
case_id=case_id,
|
||||||
|
block_id=body.get("block_id", "block-yod"),
|
||||||
|
feedback_text=body.get("feedback_text", ""),
|
||||||
|
category=category,
|
||||||
|
lesson_extracted=body.get("lesson_extracted", ""),
|
||||||
|
)
|
||||||
|
|
||||||
|
return {"id": str(feedback_id), "status": "created"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.patch("/api/feedback/{feedback_id}/resolve")
|
||||||
|
async def api_resolve_feedback(feedback_id: str, body: dict):
|
||||||
|
"""Mark feedback as resolved."""
|
||||||
|
await db.resolve_chair_feedback(
|
||||||
|
feedback_id=UUID(feedback_id),
|
||||||
|
applied_to=body.get("applied_to", []),
|
||||||
|
)
|
||||||
|
return {"status": "resolved"}
|
||||||
|
|
||||||
|
|
||||||
# ── Background Processing ─────────────────────────────────────────
|
# ── Background Processing ─────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user