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.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",
|
||||||
|
|||||||
149
src/worker.ts
149
src/worker.ts
@@ -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: "📂 תיק חדש",
|
||||||
|
|||||||
Reference in New Issue
Block a user