The /aggregate-arguments endpoint ran an in-container BackgroundTask that called claude_session (the local `claude` CLI) — which does not exist in the FastAPI container. The button silently produced nothing, and on `force` it destructively DELETEd existing arguments *before* the doomed LLM call. Replace the inline task with the established delegation pattern used by "חלץ עובדות שמאיות" (extract-appraiser-facts): a cheap in-container DB pre-check (no_claims / exists), then a Paperclip wakeup of the company's legal-analyst, which runs mcp__legal-ai__aggregate_claims_to_arguments locally (where the CLI lives) and reports back. `force` now runs locally too, so delete+recompute are atomic on the host — no more destructive failure. Frontend: AggregateArgumentsResult becomes a discriminated union (queued | no_claims | exists | skipped) and the toast is status-accurate instead of the misleading fixed "refresh in a minute". Invariants: G12 (Paperclip touch confined to paperclip_client behind the agent_platform_port), G2 (replaces the broken path, no parallel capability), engineering §6 (explicit statuses, no silent swallow). UI change is logic/toast only (no visual-layout change) — within the Claude-Design-gate bug-fix exemption. Richer inline status panels deferred to the gate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
138 lines
3.7 KiB
TypeScript
138 lines
3.7 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;
|
|
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",
|
|
];
|