שלושה כשלים בג'וב 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>
326 lines
7.5 KiB
TypeScript
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[];
|
|
}
|