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 = { 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 = { 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);