Files
plugin-legal-ai/src/legal-api.ts
Chaim Marcus 0a134d0134 fix(sync): מיפוי-הסטטוסים נגזר מ-/api/status-model, וסריקה בכל החברות (legal-ai #604)
שלושה כשלים בג'וב sync-case-status, שהשביתו אותו בשקט מאז 22.7:

1. המפה הקשיחה CASE_STATUS_TO_ISSUE_STATUS נותקה מ-case_status_model.py
   (מקור-האמת בצד legal-ai): חסרו analyst_verified ו-research_complete
   שנוספו ב-851a4fb, ונשארו בה 3 מפתחות מתים (uploading/brainstorming/drafting).
   סטטוס חסר נפל ל-`continue` — בלי לוג ובלי שגיאה. כעת המיפוי נגזר מ-
   GET /api/status-model דרך resolveIssueStatus/labelFor: terminal→done,
   הסטטוס הראשון בסדר→todo, השאר→in_progress. סטטוס חדש בצד legal-ai לא
   דורש עוד עריכה כאן (G2 — מקור-אמת אחד).
2. companies[0] בלבד — מתוך שתי החברות, אחת מעולם לא נסרקה (נמדד: 135 מול
   67 issues מקושרי-תיק). כעת נסרקות כל החברות, וה-companyId של המועמד
   שנבחר הוא זה שנכתב אליו — לא החברה הראשונה גורפת.
3. דילוג שקט על סטטוס לא-מוכר → ctx.logger.warn עם שם-הסטטוס ומספר-התיק.

אימות מול השרת החי: 0 סטיות מול המפה הישנה על כל 10 הסטטוסים שכיסתה,
+2 שכוסו לראשונה. סימולציה על הנתונים החיים: דילוג-שקט 3→0, שורות-לוג 3→6,
כתיבות בפועל 0→0 — כלומר אין נסיגה ל-flip-flop של #446.

לוגיקת בחירת-היעד (#446) לא שונתה: pickSyncTargetIssue, isWritableStatus,
CLOSED_ISSUE_STATUSES ו-NON_WRITABLE_STATUSES זהות; ל-SyncCandidate נוסף שדה
companyId בלבד (passthrough). 8 הטסטים הקיימים עוברים ללא שינוי באסרשנים,
+7 חדשים.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 01:32:24 +03:00

326 lines
7.5 KiB
TypeScript

/**
* HTTP client for Ezer Mishpati legal-ai REST API.
*/
/**
* Where the client gets its base URL. A plain string still works; a resolver is
* used when the URL comes from company-scoped plugin config, which cannot be
* read at worker startup (see worker.ts — `ctx.config.get` needs a company).
*/
export type BaseUrlSource = string | (() => string | Promise<string>);
export class LegalApi {
constructor(private baseUrl: BaseUrlSource) {}
private async resolveBaseUrl(): Promise<string> {
return typeof this.baseUrl === "string" ? this.baseUrl : this.baseUrl();
}
private async request<T>(path: string, init?: RequestInit): Promise<T> {
const base = await this.resolveBaseUrl();
const res = await fetch(`${base}${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 getStatusModel(): Promise<StatusModel> {
return this.request("/api/status-model");
}
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 StatusModelEntry {
key: string;
label: string;
description: string;
phase: string;
selectable: boolean;
terminal: boolean;
on_enter: string | null;
}
export interface StatusModel {
statuses: StatusModelEntry[];
phases: Array<{ key: string; label: 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[];
}