feat(hearing): פאנל "מה קרה בדיון" — חשיפת ניתוח-הפרוטוקול ב-UI (#226)
תוצר analyze_protocol (איך הטענות הכתובות התפתחו בדיון בעל-פה) היה שכבת ידע-תיק ללא endpoint ותצוגה — נצרך רק downstream ע"י הכותב בבלוק-י. פאנל חדש בתחתית טאב "טיעונים ועמדות", מתחת לכרטיס-הטיעונים (INV-IA1: לא טאב-מקביל). Backend: - SCHEMA_V51: cases.hearing_attendees JSONB — בית קנוני לנוכחות-בדיון (panel_members/appellants_present/respondents_present). נבדל סמנטית מ-decisions.panel_members (מותב חותם-ההחלטה) → אין מסלול-מקביל (G2). - protocol_analyzer._extract_header: מאכלס hearing_attendees; refresh על re-run (מתקן #223), לא דורס עם חילוץ ריק. אישור-משתמש: "אפשרות א" (persist+הצגה). - GET /api/cases/{n}/protocol-analysis — header (תאריך+נוכחים קנוניים) + verdicts מקובצים לפי צד. קריאה-בלבד, container-safe, ריק≠שגיאה. - POST /api/cases/{n}/analyze-protocol — מעיר את המנתח (analyze_protocol מריץ claude מקומי, נעדר בקונטיינר) כתבנית aggregate-arguments. - מגע-Paperclip רק דרך agent_platform_port (G12): wake_analyst_for_protocol_analysis. Frontend: - lib/api/protocol-analysis.ts (hooks read+trigger) + HearingChangesPanel (סרגל-כותרת, פסי-סינון התחזק/נטען-לראשונה, כרטיסים לפי צד, ציטוט-ראיה). - חיווט לטאב arguments מתחת ל-LegalArgumentsPanel. עיצוב: מוקאפ 27-case-hearing-changes-panel אושר ע"י חיים דרך שער Claude Design (X17). Invariants: מקיים G2 (בית קנוני יחיד, לא מסלול-מקביל), G1 (נרמול-במקור), G12 (שער-הפלטפורמה), INV-IA1 (לא טאב-מקביל), INV-AH (evidence_quote כבר נאכף במקור). אימות: py_compile + tsc --noEmit + eslint ירוקים; runtime של ה-endpoint = post-deploy. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
92
web/app.py
92
web/app.py
@@ -79,6 +79,7 @@ from web.agent_platform_port import (
|
||||
pc_restore_project,
|
||||
pc_wake_analyst_for_appraiser_facts,
|
||||
pc_wake_analyst_for_argument_aggregation,
|
||||
pc_wake_analyst_for_protocol_analysis,
|
||||
rename_case_project,
|
||||
pc_wake_ceo,
|
||||
pc_wake_ceo_for_action,
|
||||
@@ -2659,6 +2660,97 @@ async def api_get_legal_arguments(case_number: str, party: str = ""):
|
||||
}
|
||||
|
||||
|
||||
@app.get("/api/cases/{case_number}/protocol-analysis")
|
||||
async def api_get_protocol_analysis(case_number: str):
|
||||
"""Hearing-protocol comparative analysis for the "מה קרה בדיון" panel (#226).
|
||||
|
||||
Read-only surface over the case-knowledge produced by ``analyze_protocol``:
|
||||
the hearing header (date + who appeared, from the canonical case columns)
|
||||
plus the per-argument verdicts (strengthened / newly_raised / dropped)
|
||||
grouped by party. Container-safe — pure DB reads, no LLM. Returns an empty
|
||||
analysis (not an error) when the analysis has not been run yet.
|
||||
"""
|
||||
case = await db.get_case_by_number(case_number)
|
||||
if not case:
|
||||
raise HTTPException(404, f"תיק {case_number} לא נמצא")
|
||||
|
||||
case_id = UUID(case["id"])
|
||||
rows = await db.list_protocol_analysis(case_id)
|
||||
|
||||
# Protocol title comes from the analysed document (all rows share one
|
||||
# document_id per idempotent replace). Resolve it via the case's documents.
|
||||
protocol_title = ""
|
||||
protocol_document_id = ""
|
||||
if rows:
|
||||
protocol_document_id = rows[0].get("document_id") or ""
|
||||
docs = await db.list_documents(case_id)
|
||||
match = next(
|
||||
(d for d in docs if str(d.get("id")) == protocol_document_id), None,
|
||||
)
|
||||
if match:
|
||||
protocol_title = match.get("title") or ""
|
||||
|
||||
attendees = case.get("hearing_attendees") or {}
|
||||
hearing_date = case.get("hearing_date")
|
||||
header = {
|
||||
"hearing_date": hearing_date.isoformat() if hearing_date else None,
|
||||
"protocol_title": protocol_title,
|
||||
"protocol_document_id": protocol_document_id,
|
||||
"panel_members": attendees.get("panel_members") or [],
|
||||
"appellants_present": attendees.get("appellants_present") or [],
|
||||
"respondents_present": attendees.get("respondents_present") or [],
|
||||
}
|
||||
|
||||
# Group verdicts by party for the panel's per-side sections, in display order.
|
||||
by_party: dict[str, list[dict]] = {}
|
||||
by_change: dict[str, int] = {}
|
||||
for r in rows:
|
||||
by_party.setdefault(r.get("party_role") or "", []).append(r)
|
||||
ct = r.get("change_type", "")
|
||||
by_change[ct] = by_change.get(ct, 0) + 1
|
||||
|
||||
return {
|
||||
"case_number": case_number,
|
||||
"total": len(rows),
|
||||
"header": header,
|
||||
"by_change": by_change,
|
||||
"by_party": by_party,
|
||||
"analysis": rows,
|
||||
}
|
||||
|
||||
|
||||
@app.post("/api/cases/{case_number}/analyze-protocol")
|
||||
async def api_analyze_protocol(case_number: str, document_id: str = ""):
|
||||
"""Queue hearing-protocol analysis by waking the legal-analyst agent (#226).
|
||||
|
||||
Same delegation rationale as ``aggregate-arguments``: ``analyze_protocol``
|
||||
calls the local ``claude`` CLI, absent in this container, so we route to the
|
||||
company's analyst rather than running a doomed in-container BackgroundTask.
|
||||
|
||||
Response: {"status": "queued", ...} or {"status": "skipped", "reason": ...}.
|
||||
"""
|
||||
case = await db.get_case_by_number(case_number)
|
||||
if not case:
|
||||
raise HTTPException(404, f"תיק {case_number} לא נמצא")
|
||||
|
||||
prefix = case_number[:1]
|
||||
company_id = (
|
||||
PAPERCLIP_COMPANIES["licensing"] if prefix == "1"
|
||||
else PAPERCLIP_COMPANIES["betterment"] if prefix in ("8", "9")
|
||||
else ""
|
||||
)
|
||||
|
||||
try:
|
||||
result = await pc_wake_analyst_for_protocol_analysis(
|
||||
case_number, company_id=company_id, document_id=document_id,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception("analyst wakeup failed for protocol analysis %s", case_number)
|
||||
raise HTTPException(500, f"לא ניתן לשלוח לאנליטיקאי: {e}")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@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."""
|
||||
|
||||
Reference in New Issue
Block a user