From b01722b1b4e922b795f009c50ba0061de6abff2d Mon Sep 17 00:00:00 2001 From: Chaim Date: Tue, 26 May 2026 13:29:04 +0000 Subject: [PATCH] feat: emit missing_precedent + export_complete webhooks to plugin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds two webhook emitters in paperclip_api.py that the plugin's onWebhook handler now routes by ``eventType``: * ``emit_missing_precedent_webhook(...)`` — fires from POST /api/missing-precedents on first insert (non-duplicate). The plugin surfaces an askUserQuestions interaction on the linked issue so Daphna can choose upload / irrelevant / defer without needing to open the legal-ai UI. * ``emit_export_complete_webhook(...)`` — fires from POST /api/cases/{n}/export-docx after a successful export. The plugin attaches a "final-decision" markdown document with a download link to the linked Paperclip issue. Both are fire-and-forget BackgroundTasks — failures are logged but never block the originating request. Company resolution follows the same 1xxx→licensing / 8-9xxx→betterment rule used by emit_case_status_webhook. Co-Authored-By: Claude Sonnet 4.6 --- web/app.py | 70 +++++++++++++++++++++++++++++++---- web/paperclip_api.py | 88 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 151 insertions(+), 7 deletions(-) diff --git a/web/app.py b/web/app.py index 54eeb2f..a0d6165 100644 --- a/web/app.py +++ b/web/app.py @@ -2545,15 +2545,42 @@ async def api_mark_final(case_number: str, filename: str): @app.post("/api/cases/{case_number}/export-docx") -async def api_export_docx(case_number: str): - """Trigger DOCX export for a case.""" +async def api_export_docx(case_number: str, background_tasks: BackgroundTasks): + """Trigger DOCX export for a case. + + On a successful export, fires a fire-and-forget webhook to the + Paperclip plugin so it can attach a "final-decision" document + (markdown body + download link) to the linked issue. + """ result = await drafting_tools.export_docx(case_number) try: data = json.loads(result) - return data except json.JSONDecodeError: raise HTTPException(500, result) + # Notify the Paperclip plugin to attach the final-decision document. + docx_filename = ( + data.get("filename") + or data.get("docx_filename") + or data.get("file") + or "" + ) + if docx_filename: + prefix = case_number[:1] + company_id = ( + PAPERCLIP_COMPANIES["licensing"] if prefix == "1" + else PAPERCLIP_COMPANIES["betterment"] if prefix in ("8", "9") + else None + ) + background_tasks.add_task( + paperclip_api.emit_export_complete_webhook, + case_number=case_number, + docx_filename=docx_filename, + company_id=company_id, + ) + + return data + @app.get("/api/documents/{doc_id}/text") async def api_document_text(doc_id: str): @@ -4976,18 +5003,33 @@ def _is_internal_committee_citation(citation: str) -> bool: @app.post("/api/missing-precedents") -async def missing_precedent_create(req: MissingPrecedentCreate): +async def missing_precedent_create( + req: MissingPrecedentCreate, background_tasks: BackgroundTasks, +): """Log a new missing precedent (status='open'). Dedupes by - (citation, cited_in_case_id) — duplicate POST returns the existing row.""" + (citation, cited_in_case_id) — duplicate POST returns the existing row. + + On first insert (non-duplicate) emits a webhook to the Paperclip + plugin so it can ask Daphna via an ``askUserQuestions`` interaction + whether to upload the missing precedent. + """ if not req.citation.strip(): raise HTTPException(400, "citation חובה") case_id: UUID | None = None - if req.case_number.strip(): - c = await db.get_case_by_number(req.case_number.strip()) + case_number_for_webhook = req.case_number.strip() + company_id_for_webhook: str | None = None + if case_number_for_webhook: + c = await db.get_case_by_number(case_number_for_webhook) if not c: raise HTTPException(404, f"תיק לא נמצא: {req.case_number}") case_id = UUID(c["id"]) + prefix = case_number_for_webhook[:1] + company_id_for_webhook = ( + PAPERCLIP_COMPANIES["licensing"] if prefix == "1" + else PAPERCLIP_COMPANIES["betterment"] if prefix in ("8", "9") + else None + ) doc_id: UUID | None = None if req.cited_in_document_id: @@ -5015,6 +5057,20 @@ async def missing_precedent_create(req: MissingPrecedentCreate): claim_quote=req.claim_quote, notes=req.notes, ) + + # Trigger plugin to ask Daphna via askUserQuestions interaction. + if case_number_for_webhook and row.get("id"): + background_tasks.add_task( + paperclip_api.emit_missing_precedent_webhook, + case_number=case_number_for_webhook, + missing_precedent_id=str(row["id"]), + citation=req.citation.strip(), + cited_by_party=req.cited_by_party, + cited_by_party_name=req.cited_by_party_name, + legal_topic=req.legal_topic, + legal_issue=req.legal_issue, + company_id=company_id_for_webhook, + ) return row diff --git a/web/paperclip_api.py b/web/paperclip_api.py index ae244df..9435f4b 100644 --- a/web/paperclip_api.py +++ b/web/paperclip_api.py @@ -100,6 +100,7 @@ async def emit_case_status_webhook( "POST", "/api/plugins/marcusgroup.legal-ai/webhooks/case-status", json={ + "eventType": "status_change", "caseNumber": case_number, "oldStatus": old_status, "newStatus": new_status, @@ -114,3 +115,90 @@ async def emit_case_status_webhook( "emit_case_status_webhook failed for case %s (%s → %s): %s", case_number, old_status, new_status, exc, ) + + +async def emit_missing_precedent_webhook( + *, + case_number: str, + missing_precedent_id: str, + citation: str, + cited_by_party: str | None = None, + cited_by_party_name: str | None = None, + legal_topic: str | None = None, + legal_issue: str | None = None, + company_id: str | None = None, + run_id: str | None = None, +) -> None: + """Tell the plugin that a missing precedent was logged for a case. + + The plugin uses this to surface an ``askUserQuestions`` interaction + on the linked Paperclip issue so the chair can decide whether to + upload the cited precedent or mark it irrelevant. + + Fire-and-forget. + """ + try: + await pc_request( + "POST", + "/api/plugins/marcusgroup.legal-ai/webhooks/case-status", + json={ + "eventType": "missing_precedent_created", + "caseNumber": case_number, + "companyId": company_id, + "timestamp": datetime.now(timezone.utc).isoformat(), + "missingPrecedent": { + "id": missing_precedent_id, + "citation": citation, + "citedByParty": cited_by_party, + "citedByPartyName": cited_by_party_name, + "legalTopic": legal_topic, + "legalIssue": legal_issue, + }, + }, + run_id=run_id, + timeout=5.0, + ) + except Exception as exc: + logger.warning( + "emit_missing_precedent_webhook failed for case %s (%s): %s", + case_number, citation, exc, + ) + + +async def emit_export_complete_webhook( + *, + case_number: str, + docx_filename: str, + docx_title: str | None = None, + company_id: str | None = None, + run_id: str | None = None, +) -> None: + """Tell the plugin that a final DOCX was exported for a case. + + The plugin uses this to attach a "final decision" document to the + linked Paperclip issue (markdown body with a download link to the + DOCX). Binary attachment is intentionally avoided — the SDK's + ``documents.upsert`` accepts text only. + + Fire-and-forget. + """ + try: + await pc_request( + "POST", + "/api/plugins/marcusgroup.legal-ai/webhooks/case-status", + json={ + "eventType": "export_complete", + "caseNumber": case_number, + "companyId": company_id, + "timestamp": datetime.now(timezone.utc).isoformat(), + "docxFilename": docx_filename, + "docxTitle": docx_title or f"החלטה סופית — {case_number}", + }, + run_id=run_id, + timeout=5.0, + ) + except Exception as exc: + logger.warning( + "emit_export_complete_webhook failed for case %s (%s): %s", + case_number, docx_filename, exc, + )