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>
This commit is contained in:
28
src/agents/soul-analyst.md
Normal file
28
src/agents/soul-analyst.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# סוכן מנתח — ניתוח חומרי מקור
|
||||
|
||||
## תפקידך
|
||||
אתה מנתח מסמכים משפטיים של תיקי ערר. אתה קורא כתבי ערר, תשובות, פרוטוקולים ותכניות — ומחלץ מהם מידע מובנה.
|
||||
|
||||
## שפה
|
||||
תמיד בעברית.
|
||||
|
||||
## מה אתה עושה
|
||||
1. **חילוץ טענות** — קורא כתבי ערר ותשובות, מזהה טענות בודדות, מסווג לפי צד (עורר/ועדה/משיב/מבקש היתר)
|
||||
2. **זיהוי תכניות** — מזהה שמות תכניות מוזכרות (תב"ע, תמ"א, תכניות מקומיות)
|
||||
3. **זיהוי פסיקה** — מזהה הפניות לפסקי דין ותקדימים
|
||||
4. **סיכום חומרים** — מכין סיכום מובנה של כל החומר לחיים
|
||||
|
||||
## כלים
|
||||
- `legal_document_text` — קריאת טקסט ממסמך
|
||||
- `legal_document_list` — רשימת מסמכים בתיק
|
||||
- `legal_search` — חיפוש סמנטי בבסיס הידע
|
||||
- `legal_find_similar` — מציאת תקדימים דומים
|
||||
- `legal_case_get` — פרטי תיק
|
||||
- `legal_case_update` — עדכון סטטוס
|
||||
|
||||
## כללים
|
||||
1. **נאמנות למקור** — כל טענה מקושרת למסמך ולמיקום בו. אל תמציא טענות
|
||||
2. **ניטרליות** — אל תעריך את חוזק הטענות. רק מזהה ומסווג
|
||||
3. **שלמות** — אל תדלג על טענות. אם פספסת — חיים יעיר
|
||||
4. **דיוק** — מספרי תיק, שמות תכניות, שמות צדדים — חייבים להיות מדויקים
|
||||
5. **עבודה שיטתית** — מסמך אחרי מסמך, שורה אחרי שורה. לא לדלג
|
||||
32
src/agents/soul-ceo.md
Normal file
32
src/agents/soul-ceo.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# סוכן מנהל — ניהול תהליך כתיבת החלטות
|
||||
|
||||
## תפקידך
|
||||
אתה מנהל את תהליך כתיבת ההחלטות בוועדת ערר לתכנון ובניה, מחוז ירושלים. אתה לא כותב — אתה מנהל את התהליך, עוקב אחרי סטטוס, מתריע כשצריך, ומקצה עבודה.
|
||||
|
||||
## שפה
|
||||
תמיד בעברית.
|
||||
|
||||
## 13 סטטוסים
|
||||
|
||||
| סטטוס | משמעות | מה עושים |
|
||||
|-------|--------|---------|
|
||||
| חדש (new) | תיק נפתח | ממתינים למסמכים |
|
||||
| העלאה (uploading) | מסמכים בהעלאה | ממתינים |
|
||||
| עיבוד (processing) | חילוץ טקסט ו-embeddings | ממתינים |
|
||||
| מסמכים מוכנים (documents_ready) | הכל מעובד | **התרע לחיים: "חומרים מוכנים, הזן תוצאה"** |
|
||||
| תוצאה הוזנה (outcome_set) | דפנה קבעה תוצאה, אין נימוק | **חיים צריך סיעור מוחות** |
|
||||
| גיבוש כיוון (brainstorming) | סיעור מוחות בתהליך | ממתינים |
|
||||
| כיוון אושר (direction_approved) | יש כיוון מאושר | **מוכן לכתיבה** |
|
||||
| כתיבה (drafting) | החלטה נכתבת בלוק-בלוק | ממתינים |
|
||||
| בדיקת איכות (qa_review) | QA רץ | ממתינים |
|
||||
| טיוטה מוכנה (drafted) | עברה QA | **התרע: "טיוטה מוכנה, בדוק ושלח לדפנה"** |
|
||||
| יוצאה (exported) | DOCX נוצר | ממתינים לדפנה |
|
||||
| נבדקה (reviewed) | דפנה הגיהה | ממתינים להעלאת גרסה סופית |
|
||||
| סופית (final) | גרסה סופית נקלטה | **הפעל לולאת למידה** |
|
||||
|
||||
## כללים
|
||||
1. לעולם אל תכתוב תוכן של החלטה — רק ניהול
|
||||
2. אל תקבע תוצאה — רק דפנה מחליטה
|
||||
3. וודא שכל שלב הושלם לפני מעבר לבא
|
||||
4. אם תיק תקוע יותר מ-48 שעות — התרע
|
||||
5. שמור על תקשורת ברורה וקצרה ב-comments
|
||||
229
src/legal-api.ts
Normal file
229
src/legal-api.ts
Normal file
@@ -0,0 +1,229 @@
|
||||
/**
|
||||
* HTTP client for Ezer Mishpati legal-ai REST API.
|
||||
*/
|
||||
|
||||
export class LegalApi {
|
||||
constructor(private baseUrl: string) {}
|
||||
|
||||
private async request<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
const res = await fetch(`${this.baseUrl}${path}`, {
|
||||
...init,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...init?.headers,
|
||||
},
|
||||
});
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
throw new Error(`Legal API ${res.status}: ${text}`);
|
||||
}
|
||||
return res.json() as Promise<T>;
|
||||
}
|
||||
|
||||
async listCases(): Promise<CaseSummary[]> {
|
||||
return this.request("/api/cases");
|
||||
}
|
||||
|
||||
async getCase(caseNumber: string): Promise<CaseDetails> {
|
||||
return this.request(`/api/cases/${encodeURIComponent(caseNumber)}/details`);
|
||||
}
|
||||
|
||||
async createCase(data: CaseCreateInput): Promise<CaseDetails> {
|
||||
return this.request("/api/cases/create", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
async updateCase(caseNumber: string, data: CaseUpdateInput): Promise<CaseDetails> {
|
||||
return this.request(`/api/cases/${encodeURIComponent(caseNumber)}`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
async getCaseStatus(caseNumber: string): Promise<WorkflowStatus> {
|
||||
return this.request(`/api/cases/${encodeURIComponent(caseNumber)}/status`);
|
||||
}
|
||||
|
||||
async search(query: string, limit = 10, sectionType = ""): Promise<SearchResult[]> {
|
||||
const params = new URLSearchParams({ query, limit: String(limit) });
|
||||
if (sectionType) params.set("section_type", sectionType);
|
||||
return this.request(`/api/search?${params}`);
|
||||
}
|
||||
|
||||
async searchCase(caseNumber: string, query: string, limit = 10): Promise<SearchResult[]> {
|
||||
const params = new URLSearchParams({ query, limit: String(limit) });
|
||||
return this.request(`/api/cases/${encodeURIComponent(caseNumber)}/search?${params}`);
|
||||
}
|
||||
|
||||
async getTemplate(caseNumber: string): Promise<{ template: string }> {
|
||||
return this.request(`/api/cases/${encodeURIComponent(caseNumber)}/template`);
|
||||
}
|
||||
|
||||
async getProcessingStatus(): Promise<ProcessingStatus> {
|
||||
return this.request("/api/processing-status");
|
||||
}
|
||||
|
||||
async health(): Promise<{ status: string }> {
|
||||
return this.request("/health");
|
||||
}
|
||||
|
||||
// ── New methods for expanded workflow ──
|
||||
|
||||
async listDocuments(caseNumber: string): Promise<DocumentInfo[]> {
|
||||
const details = await this.getCase(caseNumber);
|
||||
return details.documents || [];
|
||||
}
|
||||
|
||||
async getDocumentText(docId: string): Promise<string> {
|
||||
return this.request(`/api/documents/${docId}/text`);
|
||||
}
|
||||
|
||||
async findSimilarCases(caseNumber: string): Promise<SearchResult[]> {
|
||||
return this.request(`/api/cases/${encodeURIComponent(caseNumber)}/search?query=similar&limit=5`);
|
||||
}
|
||||
|
||||
async setOutcome(caseNumber: string, outcome: string, reasoning?: string): Promise<{ status: string }> {
|
||||
return this.request(`/api/cases/${encodeURIComponent(caseNumber)}/outcome`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ outcome, reasoning: reasoning || "" }),
|
||||
});
|
||||
}
|
||||
|
||||
async getClaims(caseNumber: string): Promise<ClaimsResponse> {
|
||||
return this.request(`/api/cases/${encodeURIComponent(caseNumber)}/claims`);
|
||||
}
|
||||
|
||||
async setDirection(caseNumber: string, directionDoc: Record<string, unknown>): Promise<{ status: string }> {
|
||||
return this.request(`/api/cases/${encodeURIComponent(caseNumber)}/direction`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ direction_doc: directionDoc }),
|
||||
});
|
||||
}
|
||||
|
||||
async runQA(caseNumber: string): Promise<QAResponse> {
|
||||
return this.request(`/api/cases/${encodeURIComponent(caseNumber)}/qa`, {
|
||||
method: "POST",
|
||||
});
|
||||
}
|
||||
|
||||
async triggerLearning(caseNumber: string): Promise<{ status: string }> {
|
||||
return this.request(`/api/cases/${encodeURIComponent(caseNumber)}/learn`, {
|
||||
method: "POST",
|
||||
});
|
||||
}
|
||||
|
||||
async getStyleGuide(): Promise<string> {
|
||||
// Return static style guide reference
|
||||
return "ראה skill-legal-decision/SKILL.md — מדריך סגנון מלא של דפנה תמיר";
|
||||
}
|
||||
}
|
||||
|
||||
// Types
|
||||
|
||||
export interface CaseSummary {
|
||||
case_number: string;
|
||||
title: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface CaseDetails {
|
||||
id: string;
|
||||
case_number: string;
|
||||
title: string;
|
||||
status: string;
|
||||
appellants: string[];
|
||||
respondents: string[];
|
||||
subject: string;
|
||||
property_address: string;
|
||||
expected_outcome: string;
|
||||
documents?: DocumentInfo[];
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface CaseCreateInput {
|
||||
case_number: string;
|
||||
title: string;
|
||||
appellants?: string[];
|
||||
respondents?: string[];
|
||||
subject?: string;
|
||||
property_address?: string;
|
||||
permit_number?: string;
|
||||
committee_type?: string;
|
||||
hearing_date?: string;
|
||||
notes?: string;
|
||||
expected_outcome?: string;
|
||||
}
|
||||
|
||||
export interface CaseUpdateInput {
|
||||
status?: string;
|
||||
title?: string;
|
||||
subject?: string;
|
||||
notes?: string;
|
||||
hearing_date?: string;
|
||||
decision_date?: string;
|
||||
tags?: string[];
|
||||
expected_outcome?: string;
|
||||
}
|
||||
|
||||
export interface DocumentInfo {
|
||||
id: string;
|
||||
title: string;
|
||||
doc_type: string;
|
||||
extraction_status: string;
|
||||
page_count?: number;
|
||||
}
|
||||
|
||||
export interface WorkflowStatus {
|
||||
case_number: string;
|
||||
title: string;
|
||||
status: string;
|
||||
documents: Array<{
|
||||
title: string;
|
||||
type: string;
|
||||
extraction: string;
|
||||
chunks: number;
|
||||
pages?: number;
|
||||
}>;
|
||||
total_documents: number;
|
||||
total_chunks: number;
|
||||
has_draft: boolean;
|
||||
draft_size_bytes: number;
|
||||
next_steps: string[];
|
||||
}
|
||||
|
||||
export interface SearchResult {
|
||||
score: number;
|
||||
case_number?: string;
|
||||
document: string;
|
||||
section: string;
|
||||
page: number;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface ProcessingStatus {
|
||||
cases: number;
|
||||
documents: number;
|
||||
pending_processing: number;
|
||||
chunks: number;
|
||||
style_corpus_entries: number;
|
||||
style_patterns: number;
|
||||
}
|
||||
|
||||
export interface ClaimsResponse {
|
||||
case_number: string;
|
||||
claims: Record<string, Array<{ party_role: string; claim_text: string; claim_index: number }>>;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface QAResponse {
|
||||
passed: boolean;
|
||||
checks: Array<{
|
||||
check_name: string;
|
||||
passed: boolean;
|
||||
severity: string;
|
||||
details: string;
|
||||
}>;
|
||||
status: string;
|
||||
}
|
||||
152
src/manifest.ts
Normal file
152
src/manifest.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
export default {
|
||||
id: "marcusgroup.legal-ai",
|
||||
apiVersion: 1,
|
||||
version: "0.1.0",
|
||||
displayName: "Ezer Mishpati - Legal AI",
|
||||
description:
|
||||
"Integration with legal decision drafting system — case management, semantic search, and workflow tracking",
|
||||
author: "Marcus Group",
|
||||
categories: ["connector"] as const,
|
||||
capabilities: [
|
||||
"events.subscribe",
|
||||
"issues.read",
|
||||
"issues.create",
|
||||
"issues.update",
|
||||
"issue.comments.create",
|
||||
"agent.tools.register",
|
||||
"http.outbound",
|
||||
"plugin.state.read",
|
||||
"plugin.state.write",
|
||||
"jobs.schedule",
|
||||
"activity.log.write",
|
||||
"companies.read",
|
||||
"projects.read",
|
||||
] as const,
|
||||
entrypoints: {
|
||||
worker: "dist/worker.js",
|
||||
},
|
||||
instanceConfigSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
legalApiBaseUrl: {
|
||||
type: "string" as const,
|
||||
default: "http://localhost:8085",
|
||||
description: "Base URL for the Ezer Mishpati API",
|
||||
},
|
||||
},
|
||||
},
|
||||
tools: [
|
||||
{
|
||||
toolKey: "legal_case_list",
|
||||
name: "legal_case_list",
|
||||
displayName: "List Legal Cases",
|
||||
description: "List all appeal cases in the legal system",
|
||||
parametersSchema: { type: "object" as const, properties: {} },
|
||||
},
|
||||
{
|
||||
toolKey: "legal_case_get",
|
||||
name: "legal_case_get",
|
||||
displayName: "Get Legal Case Details",
|
||||
description: "Get full details of a legal case including documents",
|
||||
parametersSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
case_number: { type: "string" as const, description: "Case number" },
|
||||
},
|
||||
required: ["case_number"],
|
||||
},
|
||||
},
|
||||
{
|
||||
toolKey: "legal_case_create",
|
||||
name: "legal_case_create",
|
||||
displayName: "Create Legal Case",
|
||||
description: "Create a new appeal case with linked Paperclip issue",
|
||||
parametersSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
case_number: { type: "string" as const },
|
||||
title: { type: "string" as const },
|
||||
appellants: { type: "array" as const, items: { type: "string" as const } },
|
||||
respondents: { type: "array" as const, items: { type: "string" as const } },
|
||||
subject: { type: "string" as const },
|
||||
property_address: { type: "string" as const },
|
||||
expected_outcome: { type: "string" as const },
|
||||
},
|
||||
required: ["case_number", "title"],
|
||||
},
|
||||
},
|
||||
{
|
||||
toolKey: "legal_case_update",
|
||||
name: "legal_case_update",
|
||||
displayName: "Update Legal Case",
|
||||
description: "Update a legal case status, title, or outcome",
|
||||
parametersSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
case_number: { type: "string" as const },
|
||||
status: { type: "string" as const },
|
||||
title: { type: "string" as const },
|
||||
expected_outcome: { type: "string" as const },
|
||||
},
|
||||
required: ["case_number"],
|
||||
},
|
||||
},
|
||||
{
|
||||
toolKey: "legal_case_status",
|
||||
name: "legal_case_status",
|
||||
displayName: "Get Case Workflow Status",
|
||||
description: "Get full workflow status: documents, drafts, next steps",
|
||||
parametersSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
case_number: { type: "string" as const },
|
||||
},
|
||||
required: ["case_number"],
|
||||
},
|
||||
},
|
||||
{
|
||||
toolKey: "legal_search",
|
||||
name: "legal_search",
|
||||
displayName: "Search Legal Precedents",
|
||||
description: "Semantic RAG search across previous decisions",
|
||||
parametersSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
query: { type: "string" as const, description: "Search query in Hebrew" },
|
||||
limit: { type: "number" as const },
|
||||
section_type: { type: "string" as const },
|
||||
},
|
||||
required: ["query"],
|
||||
},
|
||||
},
|
||||
{
|
||||
toolKey: "legal_case_template",
|
||||
name: "legal_case_template",
|
||||
displayName: "Get Decision Template",
|
||||
description: "Get outcome-aware decision template for 12-block structure",
|
||||
parametersSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
case_number: { type: "string" as const },
|
||||
},
|
||||
required: ["case_number"],
|
||||
},
|
||||
},
|
||||
{
|
||||
toolKey: "legal_processing_status",
|
||||
name: "legal_processing_status",
|
||||
displayName: "Get Processing Status",
|
||||
description: "Get overall system processing status",
|
||||
parametersSchema: { type: "object" as const, properties: {} },
|
||||
},
|
||||
],
|
||||
jobs: [
|
||||
{
|
||||
jobKey: "sync-case-status",
|
||||
displayName: "Sync Legal Case Status",
|
||||
description:
|
||||
"Polls legal-ai for case status changes and updates Paperclip issues",
|
||||
schedule: "*/15 * * * *",
|
||||
},
|
||||
],
|
||||
};
|
||||
602
src/worker.ts
Normal file
602
src/worker.ts
Normal file
@@ -0,0 +1,602 @@
|
||||
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);
|
||||
Reference in New Issue
Block a user