מוסיף כלי-plugin שהסוכן קורא בעת continuation כדי לראות מה ריצות קודמות על התיק
הסיקו (ה-summary של כל heartbeat), במקום לגלות-מחדש הקשר מאפס (blind heartbeat).
- legal-api.ts: getPredecessorForCase(caseNumber) → GET
/api/operations/cases/{case}/predecessor (endpoint שנוסף ב-legal-ai #410).
- worker.ts: רישום הכלי (case_number param) — מעצב digest קריא (agent·issue·when·
status + summary חתוך ל-600 תווים, newest-first).
ארכיטקטורה (ההכרעה מהדיון): הכלי חי ב-plugin — המעטפת המוצהרת שמותר לה לדבר גם
Paperclip וגם legal-ai web — ולא ב-mcp-server (הליבה נשארת עיוורת-פלטפורמה, G12).
זו הסיבה שזה plugin-tool ולא mcp→web seam.
בנייה: tsc + biome נקיים. פריסה (ידנית, pm2): npm run build → plugin install →
pm2 restart paperclip. המשך: הוראת-HEARTBEAT לקרוא בעת continuation.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
295 lines
6.6 KiB
TypeScript
295 lines
6.6 KiB
TypeScript
/**
|
|
* 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 — מדריך סגנון מלא של דפנה תמיר";
|
|
}
|
|
|
|
/**
|
|
* Recent conclusions of prior heartbeat runs across a case's issues (#220,
|
|
* "seance"). Lets a resuming agent read what earlier sessions concluded
|
|
* instead of re-deriving context from scratch.
|
|
*/
|
|
async getPredecessorForCase(
|
|
caseNumber: string,
|
|
limit = 5,
|
|
): Promise<PredecessorResponse> {
|
|
return this.request(
|
|
`/api/operations/cases/${encodeURIComponent(caseNumber)}/predecessor?limit=${limit}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
export interface PredecessorRun {
|
|
run_id: string;
|
|
status: string;
|
|
started_at: string | null;
|
|
finished_at: string | null;
|
|
summary: string | null;
|
|
error_code: string | null;
|
|
session_id: string | null;
|
|
agent_name: string | null;
|
|
identifier: string | null;
|
|
}
|
|
|
|
export interface PredecessorResponse {
|
|
ok: boolean;
|
|
case_number: string;
|
|
runs: PredecessorRun[];
|
|
}
|