Files
plugin-legal-ai/src/worker.ts
2026-04-13 16:54:55 +00:00

618 lines
17 KiB
TypeScript
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()) 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: `תיק ערר חדש\ושא: ${input.subject || ""}\וצאה צפויה: ${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);