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:
@@ -15,6 +15,7 @@ export default {
|
||||
"issue.comments.read",
|
||||
"issue.comments.create",
|
||||
"issue.interactions.create",
|
||||
"issue.documents.write",
|
||||
"agent.tools.register",
|
||||
"agents.invoke",
|
||||
"http.outbound",
|
||||
|
||||
149
src/worker.ts
149
src/worker.ts
@@ -878,27 +878,47 @@ const plugin = definePlugin({
|
||||
|
||||
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 {
|
||||
eventType?: string;
|
||||
caseNumber: string;
|
||||
oldStatus: string;
|
||||
newStatus: string;
|
||||
companyId: string | null;
|
||||
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", {
|
||||
eventType,
|
||||
caseNumber,
|
||||
newStatus,
|
||||
companyId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
pluginCtx.logger.info(
|
||||
`Webhook: case ${caseNumber} ${oldStatus} → ${newStatus}`,
|
||||
`Webhook: case ${caseNumber} eventType=${eventType}`,
|
||||
{
|
||||
companyId,
|
||||
},
|
||||
@@ -927,6 +947,123 @@ const plugin = definePlugin({
|
||||
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)
|
||||
const statusLabels: Record<string, string> = {
|
||||
new: "📂 תיק חדש",
|
||||
|
||||
Reference in New Issue
Block a user