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:
232
web/app.py
232
web/app.py
@@ -326,6 +326,238 @@ async def api_processing_status():
|
||||
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 ────────────────────────────────────────────
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user