Files
plugin-legal-ai/dist/worker.js
Chaim Marcus 587a2a76ca Initial commit: Paperclip plugin for Legal AI integration
16 agent tools, event handler for auto-linking, sync job every 15m.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 08:03:43 +00:00

312 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { definePlugin, runWorker } from "@paperclipai/plugin-sdk";
import { LegalApi } from "./legal-api.js";
const plugin = definePlugin({
async setup(ctx) {
const config = (await ctx.config.get());
const baseUrl = config?.legalApiBaseUrl || "http://localhost:8085";
const api = new LegalApi(baseUrl);
ctx.logger.info("Legal AI plugin starting", { url: baseUrl });
// ── Tools ──────────────────────────────────────────────────────
ctx.tools.register("legal_case_list", {
displayName: "רשימת תיקי ערר",
description: "List all appeal cases in the legal system. Returns case number, title, and status (new/in_progress/drafted/reviewed/final).",
parametersSchema: {
type: "object",
properties: {},
},
}, async () => {
const cases = await api.listCases();
return {
content: JSON.stringify(cases, null, 2),
data: cases,
};
});
ctx.tools.register("legal_case_get", {
displayName: "פרטי תיק ערר",
description: "Get full details of a legal case including documents list. Provide the case number (e.g. 123/24).",
parametersSchema: {
type: "object",
properties: {
case_number: {
type: "string",
description: "Case number (e.g. 123/24)",
},
},
required: ["case_number"],
},
}, async (params) => {
const { case_number } = params;
const result = await api.getCase(case_number);
return {
content: JSON.stringify(result, null, 2),
data: result,
};
});
ctx.tools.register("legal_case_create", {
displayName: "יצירת תיק ערר",
description: "Create a new appeal case. Case numbers: 1xxx=licensing, 8xxx=betterment levy, 9xxx=compensation. Also creates a linked Paperclip issue.",
parametersSchema: {
type: "object",
properties: {
case_number: {
type: "string",
description: "Case number (e.g. 1234/24)",
},
title: { type: "string", description: "Case title" },
appellants: {
type: "array",
items: { type: "string" },
description: "Appellant names",
},
respondents: {
type: "array",
items: { type: "string" },
description: "Respondent names",
},
subject: { type: "string", description: "Case subject" },
property_address: {
type: "string",
description: "Property address",
},
expected_outcome: {
type: "string",
enum: [
"rejection",
"partial_acceptance",
"full_acceptance",
"betterment_levy",
],
description: "Expected outcome type",
},
},
required: ["case_number", "title"],
},
}, async (params, runCtx) => {
const input = params;
// Create case in legal-ai
const legalCase = await api.createCase(input);
// Create linked Paperclip issue
const issue = await ctx.issues.create({
companyId: runCtx.companyId,
title: `[ערר ${input.case_number}] ${input.title}`,
description: `תיק ערר חדש\nנושא: ${input.subject || ""}\nתוצאה צפויה: ${input.expected_outcome || "לא הוגדרה"}`,
});
// Store mapping in plugin state
await ctx.state.set({
scopeKind: "issue",
scopeId: issue.id,
stateKey: "legal-case-number",
}, input.case_number);
await ctx.activity.log({
companyId: runCtx.companyId,
message: `נוצר תיק ערר ${input.case_number} וקושר ל-issue ${issue.id}`,
});
return {
content: `Case ${input.case_number} created and linked to Paperclip issue.\n\n${JSON.stringify(legalCase, null, 2)}`,
data: { legalCase, issueId: issue.id },
};
});
ctx.tools.register("legal_case_update", {
displayName: "עדכון תיק ערר",
description: "Update a legal case's status, title, subject, or expected outcome.",
parametersSchema: {
type: "object",
properties: {
case_number: {
type: "string",
description: "Case number (e.g. 123/24)",
},
status: {
type: "string",
enum: [
"new",
"in_progress",
"drafted",
"reviewed",
"final",
],
description: "New case status",
},
title: { type: "string" },
subject: { type: "string" },
notes: { type: "string" },
expected_outcome: {
type: "string",
enum: [
"rejection",
"partial_acceptance",
"full_acceptance",
"betterment_levy",
],
},
},
required: ["case_number"],
},
}, async (params) => {
const { case_number, ...updates } = params;
const result = await api.updateCase(case_number, updates);
return {
content: JSON.stringify(result, null, 2),
data: result,
};
});
ctx.tools.register("legal_case_status", {
displayName: "סטטוס תהליך עבודה",
description: "Get full workflow status for a case: documents processed, chunks created, draft progress, and suggested next steps.",
parametersSchema: {
type: "object",
properties: {
case_number: {
type: "string",
description: "Case number (e.g. 123/24)",
},
},
required: ["case_number"],
},
}, async (params) => {
const { case_number } = params;
const result = await api.getCaseStatus(case_number);
return {
content: JSON.stringify(result, null, 2),
data: result,
};
});
ctx.tools.register("legal_search", {
displayName: "חיפוש תקדימים משפטיים",
description: "Semantic search (RAG) across previous decisions and documents. Query in Hebrew for best results.",
parametersSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "Search query in Hebrew",
},
limit: { type: "number", description: "Max results (default 10)" },
section_type: {
type: "string",
description: "Filter by section type: facts, legal_analysis, conclusion, ruling",
},
},
required: ["query"],
},
}, async (params) => {
const { query, limit, section_type } = params;
const results = await api.search(query, limit || 10, section_type || "");
return {
content: JSON.stringify(results, null, 2),
data: results,
};
});
ctx.tools.register("legal_case_template", {
displayName: "תבנית החלטה",
description: "Get an outcome-aware decision template for a case, with guidance for the 12-block structure.",
parametersSchema: {
type: "object",
properties: {
case_number: {
type: "string",
description: "Case number (e.g. 123/24)",
},
},
required: ["case_number"],
},
}, async (params) => {
const { case_number } = params;
const result = await api.getTemplate(case_number);
return {
content: result.template,
data: result,
};
});
ctx.tools.register("legal_processing_status", {
displayName: "סטטוס עיבוד כללי",
description: "Get overall processing status: total cases, documents, pending processing, chunks, and style corpus entries.",
parametersSchema: {
type: "object",
properties: {},
},
}, async () => {
const result = await api.getProcessingStatus();
return {
content: JSON.stringify(result, null, 2),
data: result,
};
});
// ── Events ─────────────────────────────────────────────────────
ctx.events.on("issue.created", async (event) => {
// Auto-link issues with case number in title
if (!event.companyId || !event.entityId)
return;
const issue = await ctx.issues.get(event.entityId, event.companyId);
if (!issue)
return;
const match = issue.title.match(/ערר\s+(\d+\/\d+)/);
if (match) {
const caseNumber = match[1];
await ctx.state.set({
scopeKind: "issue",
scopeId: issue.id,
stateKey: "legal-case-number",
}, caseNumber);
ctx.logger.info("Auto-linked issue to legal case", {
issueId: issue.id,
caseNumber,
});
}
});
// ── Jobs ───────────────────────────────────────────────────────
ctx.jobs.register("sync-case-status", async (job) => {
ctx.logger.info("Starting case status sync", { runId: job.runId });
try {
const cases = await api.listCases();
const companies = await ctx.companies.list();
if (!companies.length)
return;
const companyId = companies[0].id;
const issues = await ctx.issues.list({ companyId });
for (const legalCase of cases) {
for (const issue of issues) {
const linkedCase = await ctx.state.get({
scopeKind: "issue",
scopeId: issue.id,
stateKey: "legal-case-number",
});
if (linkedCase === legalCase.case_number) {
// Map legal-ai status to Paperclip issue status
const statusMap = {
new: "todo",
in_progress: "in_progress",
drafted: "in_progress",
reviewed: "in_progress",
final: "done",
};
const targetStatus = statusMap[legalCase.status];
if (targetStatus && issue.status !== targetStatus) {
await ctx.issues.update(issue.id, { status: targetStatus }, companyId);
await ctx.issues.createComment(issue.id, `סטטוס תיק עודכן ל: ${legalCase.status}`, companyId);
ctx.logger.info("Synced issue status", {
issueId: issue.id,
caseNumber: legalCase.case_number,
newStatus: targetStatus,
});
}
}
}
}
ctx.logger.info("Case status sync completed", {
casesChecked: cases.length,
});
}
catch (err) {
ctx.logger.error("Case status sync failed", { error: String(err) });
}
});
ctx.logger.info("Legal AI plugin ready");
},
async onHealth() {
return { status: "ok" };
},
});
export default plugin;
runWorker(plugin, import.meta.url);