618 lines
17 KiB
TypeScript
618 lines
17 KiB
TypeScript
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()) as {
|
||
legalApiBaseUrl?: string;
|
||
} | null;
|
||
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 as { case_number: string };
|
||
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 as {
|
||
case_number: string;
|
||
title: string;
|
||
appellants?: string[];
|
||
respondents?: string[];
|
||
subject?: string;
|
||
property_address?: string;
|
||
expected_outcome?: string;
|
||
};
|
||
|
||
// 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 as {
|
||
case_number: string;
|
||
status?: string;
|
||
title?: string;
|
||
subject?: string;
|
||
notes?: string;
|
||
expected_outcome?: string;
|
||
};
|
||
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 as { case_number: string };
|
||
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 as {
|
||
query: string;
|
||
limit?: number;
|
||
section_type?: string;
|
||
};
|
||
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 as { case_number: string };
|
||
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,
|
||
};
|
||
},
|
||
);
|
||
|
||
// ── New Tools (Phase 3) ─────────────────────────────────────────
|
||
|
||
ctx.tools.register(
|
||
"legal_document_list",
|
||
{
|
||
displayName: "רשימת מסמכים בתיק",
|
||
description:
|
||
"List all documents in a case with their extraction status.",
|
||
parametersSchema: {
|
||
type: "object",
|
||
properties: {
|
||
case_number: { type: "string", description: "Case number" },
|
||
},
|
||
required: ["case_number"],
|
||
},
|
||
},
|
||
async (params) => {
|
||
const { case_number } = params as { case_number: string };
|
||
const docs = await api.listDocuments(case_number);
|
||
return { content: JSON.stringify(docs, null, 2), data: docs };
|
||
},
|
||
);
|
||
|
||
ctx.tools.register(
|
||
"legal_set_outcome",
|
||
{
|
||
displayName: "הזנת תוצאת ערר",
|
||
description:
|
||
"Set the decision outcome (rejection/full_acceptance/partial_acceptance) and optional reasoning from Dafna.",
|
||
parametersSchema: {
|
||
type: "object",
|
||
properties: {
|
||
case_number: { type: "string", description: "Case number" },
|
||
outcome: {
|
||
type: "string",
|
||
enum: ["rejection", "full_acceptance", "partial_acceptance"],
|
||
description: "Decision outcome",
|
||
},
|
||
reasoning: {
|
||
type: "string",
|
||
description: "Optional reasoning from Dafna",
|
||
},
|
||
},
|
||
required: ["case_number", "outcome"],
|
||
},
|
||
},
|
||
async (params) => {
|
||
const { case_number, outcome, reasoning } = params as {
|
||
case_number: string;
|
||
outcome: string;
|
||
reasoning?: string;
|
||
};
|
||
const result = await api.setOutcome(case_number, outcome, reasoning);
|
||
return { content: JSON.stringify(result, null, 2), data: result };
|
||
},
|
||
);
|
||
|
||
ctx.tools.register(
|
||
"legal_get_claims",
|
||
{
|
||
displayName: "טענות מחולצות",
|
||
description: "Get extracted claims for a case, grouped by party role.",
|
||
parametersSchema: {
|
||
type: "object",
|
||
properties: {
|
||
case_number: { type: "string", description: "Case number" },
|
||
},
|
||
required: ["case_number"],
|
||
},
|
||
},
|
||
async (params) => {
|
||
const { case_number } = params as { case_number: string };
|
||
const result = await api.getClaims(case_number);
|
||
return { content: JSON.stringify(result, null, 2), data: result };
|
||
},
|
||
);
|
||
|
||
ctx.tools.register(
|
||
"legal_search_case",
|
||
{
|
||
displayName: "חיפוש בתוך תיק",
|
||
description: "Semantic search within a specific case's documents.",
|
||
parametersSchema: {
|
||
type: "object",
|
||
properties: {
|
||
case_number: { type: "string", description: "Case number" },
|
||
query: { type: "string", description: "Search query in Hebrew" },
|
||
},
|
||
required: ["case_number", "query"],
|
||
},
|
||
},
|
||
async (params) => {
|
||
const { case_number, query } = params as {
|
||
case_number: string;
|
||
query: string;
|
||
};
|
||
const results = await api.searchCase(case_number, query);
|
||
return { content: JSON.stringify(results, null, 2), data: results };
|
||
},
|
||
);
|
||
|
||
ctx.tools.register(
|
||
"legal_find_similar",
|
||
{
|
||
displayName: "תקדימים דומים",
|
||
description: "Find similar cases/precedents for a given case.",
|
||
parametersSchema: {
|
||
type: "object",
|
||
properties: {
|
||
case_number: { type: "string", description: "Case number" },
|
||
},
|
||
required: ["case_number"],
|
||
},
|
||
},
|
||
async (params) => {
|
||
const { case_number } = params as { case_number: string };
|
||
const results = await api.findSimilarCases(case_number);
|
||
return { content: JSON.stringify(results, null, 2), data: results };
|
||
},
|
||
);
|
||
|
||
ctx.tools.register(
|
||
"legal_run_qa",
|
||
{
|
||
displayName: "בדיקת איכות",
|
||
description:
|
||
"Run QA validation on a drafted decision. Checks: grounding, claims coverage, neutral background, weights.",
|
||
parametersSchema: {
|
||
type: "object",
|
||
properties: {
|
||
case_number: { type: "string", description: "Case number" },
|
||
},
|
||
required: ["case_number"],
|
||
},
|
||
},
|
||
async (params) => {
|
||
const { case_number } = params as { case_number: string };
|
||
const result = await api.runQA(case_number);
|
||
return { content: JSON.stringify(result, null, 2), data: result };
|
||
},
|
||
);
|
||
|
||
ctx.tools.register(
|
||
"legal_trigger_learning",
|
||
{
|
||
displayName: "הפעלת לולאת למידה",
|
||
description:
|
||
"Trigger the learning loop — compare draft to final signed version.",
|
||
parametersSchema: {
|
||
type: "object",
|
||
properties: {
|
||
case_number: { type: "string", description: "Case number" },
|
||
},
|
||
required: ["case_number"],
|
||
},
|
||
},
|
||
async (params) => {
|
||
const { case_number } = params as { case_number: string };
|
||
const result = await api.triggerLearning(case_number);
|
||
return { content: JSON.stringify(result, null, 2), data: result };
|
||
},
|
||
);
|
||
|
||
ctx.tools.register(
|
||
"legal_style_guide",
|
||
{
|
||
displayName: "מדריך סגנון",
|
||
description: "Get reference to Dafna's writing style guide.",
|
||
parametersSchema: {
|
||
type: "object",
|
||
properties: {},
|
||
},
|
||
},
|
||
async () => {
|
||
const guide = await api.getStyleGuide();
|
||
return { content: guide, data: { reference: guide } };
|
||
},
|
||
);
|
||
|
||
// ── 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 13 legal-ai statuses to Paperclip issue status
|
||
const statusMap: Record<string, "todo" | "in_progress" | "done"> =
|
||
{
|
||
new: "todo",
|
||
uploading: "todo",
|
||
processing: "in_progress",
|
||
documents_ready: "in_progress",
|
||
outcome_set: "in_progress",
|
||
brainstorming: "in_progress",
|
||
direction_approved: "in_progress",
|
||
drafting: "in_progress",
|
||
qa_review: "in_progress",
|
||
drafted: "in_progress",
|
||
exported: "in_progress",
|
||
reviewed: "in_progress",
|
||
final: "done",
|
||
};
|
||
|
||
const statusLabels: Record<string, string> = {
|
||
new: "תיק חדש",
|
||
uploading: "העלאת מסמכים",
|
||
processing: "עיבוד מסמכים",
|
||
documents_ready: "מסמכים מוכנים — הזן תוצאה",
|
||
outcome_set: "תוצאה הוזנה — נדרש סיעור מוחות",
|
||
brainstorming: "גיבוש כיוון בתהליך",
|
||
direction_approved: "כיוון אושר — מוכן לכתיבה",
|
||
drafting: "כתיבת החלטה בתהליך",
|
||
qa_review: "בדיקת איכות",
|
||
drafted: "טיוטה מוכנה — בדוק ושלח לדפנה",
|
||
exported: "DOCX נוצר — ממתין לדפנה",
|
||
reviewed: "דפנה הגיהה — העלה גרסה סופית",
|
||
final: "גרסה סופית — לולאת למידה",
|
||
};
|
||
|
||
const targetStatus = statusMap[legalCase.status];
|
||
const label = statusLabels[legalCase.status] || legalCase.status;
|
||
|
||
if (targetStatus && issue.status !== targetStatus) {
|
||
await ctx.issues.update(
|
||
issue.id,
|
||
{ status: targetStatus },
|
||
companyId,
|
||
);
|
||
await ctx.issues.createComment(
|
||
issue.id,
|
||
`📋 ${label}`,
|
||
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" as const };
|
||
},
|
||
});
|
||
|
||
export default plugin;
|
||
runWorker(plugin, import.meta.url);
|