Files
legal-ai/web-ui/src/lib/api/legal-arguments.ts
Chaim 68c2be2a7b
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 11m48s
Lint — undefined names / undefined-names (pull_request) Successful in 12s
feat(arguments): הפרדת-משיבים בצבירה לפי כתב-תשובה (#224)
הצבירה קיבצה את כל המשיבים תחת מפלגה אחת "respondent", כך שכתבי-תשובה
נפרדים (משיבות 2-3 מול משיבים 4-6) עם עמדות שעלולות להיות מנוגדות נבלעו
לרשימה אחת. התגלה בתיק 1043-02-26.

השינוי מפצל את הצדדים הרב-משתתפים (respondent/permit_applicant) לפי
source_document — כל כתב-תשובה משותף = יחידת-ליטיגציה קוהרנטית נפרדת —
ומאחסן את התווית ב-legal_arguments.party_name (מיגרציה V50). עוררים/ועדה
נשארים קבוצה יחידה. בונוס: פיצול ה-129 טענות ל-קריאות-Claude קטנות מתקן
את הכשל-בשקט המקורי (קריאה של 100+ פרופוזיציות החזירה non-JSON והפילה את
כל הצד).

- db.py: SCHEMA_V50_SQL — legal_arguments.party_name (ADD COLUMN IF NOT EXISTS, אידמפוטנטי)
- argument_aggregator: קיבוץ (party, party_name); _build_prompt מקבל שם-כתב; SELECT מחזיר party_name
- UI: PartySection מתת-קבץ לפי כתב-תשובה; טיפוס LegalArgument.party_name

טווח v1: הקיבוץ לפי source_document; אכלוס party_name ב-extractor וזיהוי
עמדות-מנוגדות בבלוק ז/י — המשך ב-#224.

Invariants: G1 (נרמול-במקור — party_name בסכמה, לא תיקון-בקריאה) · G2 (אין
מסלול-צבירה מקביל, אותו pipeline) · §6 (אין בליעה שקטה — הכשל-בשקט תוקן).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 09:42:09 +00:00

144 lines
3.9 KiB
TypeScript

/**
* Legal Arguments domain — aggregated propositions (claim de-dup).
*
* Each raw "claim" is an extracted proposition from a litigation brief;
* the LLM-driven aggregator groups them by party into 6-12 distinct
* legal arguments. These hooks expose the read + trigger endpoints.
*/
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "./client";
export type LegalArgumentParty =
| "appellant"
| "respondent"
| "committee"
| "permit_applicant"
| "unknown";
export type LegalArgumentPriority =
| "threshold"
| "substantive"
| "procedural"
| "relief";
export type SupportingProposition = {
id: string;
text: string;
source_document: string | null;
};
export type LegalArgument = {
id: string;
case_id: string;
party: LegalArgumentParty;
/**
* The specific pleading this argument belongs to, for sides that may comprise
* several litigants with opposing positions (respondent / permit_applicant
* briefs). Empty for single-voice sides (appellant / committee). (#224)
*/
party_name?: string;
argument_index: number;
argument_title: string;
argument_body: string;
legal_topic: string | null;
priority: LegalArgumentPriority;
cited_precedents?: string[] | null;
created_at?: string;
updated_at?: string;
supporting_claims: string[];
/** Raw extracted propositions (id + full text) backing this argument. */
supporting_propositions?: SupportingProposition[];
};
export type LegalArgumentsResponse = {
case_number: string;
total: number;
by_party: Partial<Record<LegalArgumentParty, LegalArgument[]>>;
arguments: LegalArgument[];
};
export const legalArgumentsKeys = {
all: ["legal-arguments"] as const,
byCase: (caseNumber: string) =>
[...legalArgumentsKeys.all, caseNumber] as const,
};
export function useLegalArguments(caseNumber: string | undefined) {
return useQuery({
queryKey: legalArgumentsKeys.byCase(caseNumber ?? ""),
queryFn: ({ signal }) =>
apiRequest<LegalArgumentsResponse>(
`/api/cases/${caseNumber}/legal-arguments`,
{ signal },
),
enabled: Boolean(caseNumber),
staleTime: 10_000,
});
}
/**
* The aggregation runs on the legal-analyst agent (host-side, where the
* `claude` CLI lives) — NOT inline in the FastAPI container. The endpoint
* either delegates via a Paperclip wakeup (`queued`) or short-circuits on a
* cheap in-container DB pre-check (`no_claims` / `exists`). `skipped` means
* no analyst route was available; the chair can run the MCP tool manually.
*/
export type AggregateArgumentsResult =
| {
status: "queued";
sub_issue_id: string;
analyst_id: string;
main_issue_id: string;
}
| {
status: "no_claims" | "exists";
total: number;
message: string;
}
| {
status: "skipped";
reason: "no_api_key" | "no_analyst" | "no_issue" | string;
company_id?: string;
};
export function useAggregateArguments(caseNumber: string | undefined) {
const qc = useQueryClient();
return useMutation({
mutationFn: (force: boolean = false) =>
apiRequest<AggregateArgumentsResult>(
`/api/cases/${caseNumber}/aggregate-arguments${force ? "?force=true" : ""}`,
{ method: "POST" },
),
onSuccess: () => {
if (caseNumber) {
qc.invalidateQueries({
queryKey: legalArgumentsKeys.byCase(caseNumber),
});
}
},
});
}
export const PARTY_LABELS_HE: Record<LegalArgumentParty, string> = {
appellant: "עוררים",
respondent: "משיבים",
committee: "ועדה מקומית",
permit_applicant: "מבקשי היתר",
unknown: "צד לא מזוהה",
};
export const PRIORITY_LABELS_HE: Record<LegalArgumentPriority, string> = {
threshold: "סף",
substantive: "מהותי",
procedural: "פגם הליך",
relief: "סעד",
};
export const PRIORITY_ORDER: LegalArgumentPriority[] = [
"threshold",
"substantive",
"procedural",
"relief",
];