feat: adopt SDK 525 features — askUserQuestions + documents.upsert

Two new event types accepted on the existing case-status webhook
(eventType discriminator added; legacy status_change still default):

* **missing_precedent_created** → ``ctx.issues.askUserQuestions``
  with a single-choice question {upload | irrelevant | defer}.
  continuationPolicy=wake_assignee_on_accept routes the chair's
  answer straight back to the CEO heartbeat without an extra hop.

* **export_complete** → ``ctx.issues.documents.upsert`` with a
  markdown "final-decision" doc that links back to the DOCX on
  legal-ai. (The SDK's documents API stores text only — binary
  attachment isn't natively supported here.)

Manifest: +issue.documents.write capability (issue.interactions.create
was already declared in the previous SDK upgrade).

Tested: plugin activates with all 18 capabilities, 8 tools + 3 jobs
+ 1 webhook + 2 event subs registered.
This commit is contained in:
2026-05-26 13:28:43 +00:00
parent 6388062cdc
commit 0ca7831c53
2 changed files with 144 additions and 6 deletions

View File

@@ -15,6 +15,7 @@ export default {
"issue.comments.read", "issue.comments.read",
"issue.comments.create", "issue.comments.create",
"issue.interactions.create", "issue.interactions.create",
"issue.documents.write",
"agent.tools.register", "agent.tools.register",
"agents.invoke", "agents.invoke",
"http.outbound", "http.outbound",

View File

@@ -878,27 +878,47 @@ const plugin = definePlugin({
if (endpointKey !== "case-status") return; if (endpointKey !== "case-status") return;
// Webhook payload is a discriminated union by `eventType`:
// • undefined / "status_change" — legacy case-status update (default)
// • "missing_precedent_created" — ask Daphna to upload a missing citation
// • "export_complete" — attach a generated DOCX to the linked issue
const payload = parsedBody as { const payload = parsedBody as {
eventType?: string;
caseNumber: string; caseNumber: string;
oldStatus: string;
newStatus: string;
companyId: string | null; companyId: string | null;
timestamp: string; timestamp: string;
// status_change fields
oldStatus?: string;
newStatus?: string;
// missing_precedent_created fields
missingPrecedent?: {
id: string;
citation: string;
citedByParty?: string;
citedByPartyName?: string;
legalTopic?: string;
legalIssue?: string;
};
// export_complete fields
docxBase64?: string;
docxFilename?: string;
docxTitle?: string;
}; };
const { caseNumber, oldStatus, newStatus, companyId } = payload; const { caseNumber, companyId } = payload;
const eventType = payload.eventType ?? "status_change";
if (!caseNumber || !newStatus || !companyId) { if (!caseNumber || !companyId) {
pluginCtx.logger.warn("onWebhook: malformed payload", { pluginCtx.logger.warn("onWebhook: malformed payload", {
eventType,
caseNumber, caseNumber,
newStatus,
companyId, companyId,
}); });
return; return;
} }
pluginCtx.logger.info( pluginCtx.logger.info(
`Webhook: case ${caseNumber} ${oldStatus}${newStatus}`, `Webhook: case ${caseNumber} eventType=${eventType}`,
{ {
companyId, companyId,
}, },
@@ -927,6 +947,123 @@ const plugin = definePlugin({
return; return;
} }
// ── Branch by eventType ────────────────────────────────────────
// EVENT: missing_precedent_created — ask Daphna to upload via SDK
// interaction (ask_user_questions). Avoids plain-text comment for
// a decision that has a clear set of choices.
if (eventType === "missing_precedent_created" && payload.missingPrecedent) {
const mp = payload.missingPrecedent;
const partyLabel = mp.citedByPartyName || mp.citedByParty || "אחד הצדדים";
try {
await pluginCtx.issues.askUserQuestions(
linkedIssueId,
{
continuationPolicy: "wake_assignee_on_accept",
payload: {
version: 1,
title: `פסיקה חסרה בקורפוס: ${mp.citation}`,
questions: [
{
id: `missing_precedent:${mp.id}`,
prompt: `יש להעלות את ${mp.citation}`,
helpText: [
`הציטוט הוזכר על-ידי ${partyLabel} בתיק ${caseNumber}.`,
mp.legalTopic ? `נושא: ${mp.legalTopic}` : "",
mp.legalIssue ? `סוגיה: ${mp.legalIssue}` : "",
"",
"כדי שהמערכת תוכל לבחון את הטענה מול הפסיקה — יש להעלות את ה-PDF/DOCX לדף /missing-precedents.",
]
.filter(Boolean)
.join("\n"),
selectionMode: "single",
required: true,
options: [
{ id: "upload", label: "אני מעלה PDF/DOCX" },
{ id: "irrelevant", label: "ההלכה לא רלוונטית" },
{ id: "defer", label: "אכריע מאוחר יותר" },
],
},
],
},
},
companyId,
);
pluginCtx.logger.info(
"askUserQuestions: missing_precedent prompt sent",
{
caseNumber,
missingPrecedentId: mp.id,
},
);
} catch (err) {
pluginCtx.logger.error(
"askUserQuestions failed for missing_precedent",
{
caseNumber,
missingPrecedentId: mp.id,
error: String(err),
},
);
}
return;
}
// EVENT: export_complete — attach a markdown "final decision"
// document to the issue, with a link back to the DOCX in legal-ai.
// The SDK's `documents.upsert` stores text (markdown), so we keep
// the binary DOCX in legal-ai and reference it; the issue page
// shows the document as a discoverable artifact.
if (eventType === "export_complete" && payload.docxFilename) {
const filename = payload.docxFilename;
const title = payload.docxTitle || `החלטה סופית — ${caseNumber}`;
const docxUrl = `https://legal-ai.nautilus.marcusgroup.org/api/cases/${encodeURIComponent(caseNumber)}/export/download?filename=${encodeURIComponent(filename)}`;
const markdownBody = [
`# ${title}`,
"",
`**תיק:** ${caseNumber}`,
`**קובץ:** \`${filename}\``,
`**הופק:** ${payload.timestamp}`,
"",
`[הורדה](${docxUrl})`,
"",
"---",
"",
'החלטה זו יוצאה אוטומטית ע"י legal-ai והוצמדה ל-issue זה.',
].join("\n");
try {
await pluginCtx.issues.documents.upsert({
issueId: linkedIssueId,
companyId,
key: `final-decision:${caseNumber}`,
body: markdownBody,
title,
format: "markdown",
changeSummary: `Auto-attached final DOCX (${filename})`,
});
pluginCtx.logger.info("documents.upsert: final-decision attached", {
caseNumber,
filename,
});
} catch (err) {
pluginCtx.logger.error("documents.upsert failed for export_complete", {
caseNumber,
filename,
error: String(err),
});
}
return;
}
// EVENT: status_change (default) — legacy behavior.
const { oldStatus = "", newStatus = "" } = payload;
if (!newStatus) {
pluginCtx.logger.warn("onWebhook status_change: missing newStatus", {
caseNumber,
});
return;
}
// Status label map (Hebrew) // Status label map (Hebrew)
const statusLabels: Record<string, string> = { const statusLabels: Record<string, string> = {
new: "📂 תיק חדש", new: "📂 תיק חדש",