refactor(precedents): keep all LLM calls on the local-MCP path
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m28s

Architectural correction: every claude_session caller in this project
runs through the local MCP server (~/.claude.json points at
/home/chaim/legal-ai/mcp-server/.venv/bin/python). The Coolify container
has no `claude` CLI and no claude.ai session, so any LLM call originating
from web/ FastAPI fails with "Claude CLI not found" — which is exactly
what we hit on 403-17.

The earlier Anthropic SDK fallback would have made it work, but at
direct API cost. The chair's preference is to stay on the claude.ai
session for everything. So:

- claude_session.py: removed the SDK fallback, restored CLI-only.
  The error message now points the next person at the architectural
  rule in the module docstring instead of papering over it.
- precedent_library.py:ingest_precedent (called from FastAPI on upload)
  now does only the non-LLM half: extract → chunk → embed → store.
  Sets halacha_extraction_status='pending' for the chair to act on.
- reextract_halachot / reextract_metadata kept, but lazy-import their
  extractors so the FastAPI path can't accidentally pull them in. They
  are reachable only via the MCP tools precedent_extract_halachot /
  precedent_extract_metadata, which run locally with CLI.
- Removed POST /api/precedent-library/{id}/extract-halachot and
  /extract-metadata — they were dead ends from the container.
- Dropped the `anthropic` Python dep that the SDK fallback required.
- UI: removed the "refresh halachot" and "sparkles metadata" buttons
  that called those endpoints. Edit sheet now points the chair at the
  MCP tool names instead.

Halacha and metadata extraction for an uploaded precedent now happen
when the chair (via Claude Code) runs:
  mcp__legal-ai__precedent_extract_metadata <case_law_id>
  mcp__legal-ai__precedent_extract_halachot <case_law_id>

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-03 11:06:08 +00:00
parent 5d836ca414
commit 2cfdf35191
7 changed files with 117 additions and 355 deletions

View File

@@ -1,7 +1,7 @@
"use client";
import { useState } from "react";
import { Trash2, Plus, RefreshCw, Pencil } from "lucide-react";
import { Trash2, Plus, Pencil } from "lucide-react";
import { toast } from "sonner";
import {
Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
@@ -16,7 +16,6 @@ import {
import {
usePrecedents,
useDeletePrecedent,
useReExtractHalachot,
type Precedent,
type PracticeArea,
} from "@/lib/api/precedent-library";
@@ -63,7 +62,6 @@ function PrecedentRow({
onEdit: (id: string) => void;
}) {
const del = useDeletePrecedent();
const reExtract = useReExtractHalachot();
const onDelete = async () => {
if (!window.confirm(`למחוק את ${p.case_number}? cascade ימחק את ה-chunks וההלכות.`)) return;
@@ -75,15 +73,6 @@ function PrecedentRow({
}
};
const onReExtract = async () => {
try {
await reExtract.mutateAsync(p.id);
toast.success("חילוץ הלכות החל");
} catch (e) {
toast.error(e instanceof Error ? e.message : "שגיאה");
}
};
return (
<TableRow className="border-rule hover:bg-gold-wash/30">
<TableCell className="font-semibold text-navy" dir="ltr">
@@ -119,15 +108,6 @@ function PrecedentRow({
>
<Pencil className="w-4 h-4" />
</Button>
<Button
variant="ghost" size="sm" onClick={onReExtract}
disabled={reExtract.isPending}
aria-label="חלץ הלכות מחדש"
title="חלץ הלכות מחדש"
className="text-ink-muted hover:text-navy"
>
<RefreshCw className="w-4 h-4" />
</Button>
<Button
variant="ghost" size="sm" onClick={onDelete}
disabled={del.isPending}

View File

@@ -1,7 +1,7 @@
"use client";
import { useEffect, useState } from "react";
import { Save, Sparkles, Loader2 } from "lucide-react";
import { Save } from "lucide-react";
import { toast } from "sonner";
import {
Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription,
@@ -17,11 +17,9 @@ import {
import {
usePrecedent,
useUpdatePrecedent,
useReExtractMetadata,
type PracticeArea,
type SourceType,
} from "@/lib/api/precedent-library";
import { useProgress } from "@/lib/api/documents";
import {
PRACTICE_AREAS, PRECEDENT_LEVELS, SOURCE_TYPES,
} from "./practice-area";
@@ -61,11 +59,8 @@ export function PrecedentEditSheet({ caseLawId, onOpenChange }: Props) {
const open = caseLawId !== null;
const { data: record, isPending } = usePrecedent(caseLawId);
const update = useUpdatePrecedent();
const reextractMeta = useReExtractMetadata();
const [form, setForm] = useState<FormState>(EMPTY);
const [metadataTaskId, setMetadataTaskId] = useState<string | null>(null);
const metadataProgress = useProgress(metadataTaskId);
// Hydrate form when the record loads.
useEffect(() => {
@@ -88,17 +83,6 @@ export function PrecedentEditSheet({ caseLawId, onOpenChange }: Props) {
});
}, [record]);
// Auto-close metadata progress on completion + refresh form
useEffect(() => {
if (metadataProgress?.status === "completed") {
toast.success("חילוץ מטא-דאטה הסתיים — השדות עודכנו");
setMetadataTaskId(null);
} else if (metadataProgress?.status === "failed") {
toast.error(`חילוץ מטא-דאטה נכשל: ${metadataProgress.error || ""}`);
setMetadataTaskId(null);
}
}, [metadataProgress]);
const onSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!caseLawId) return;
@@ -128,21 +112,6 @@ export function PrecedentEditSheet({ caseLawId, onOpenChange }: Props) {
}
};
const onTriggerMetadata = async () => {
if (!caseLawId) return;
try {
const res = await reextractMeta.mutateAsync(caseLawId);
setMetadataTaskId(res.task_id);
toast.message("מחלץ מטא-דאטה ברקע…");
} catch (err) {
toast.error(err instanceof Error ? err.message : "שגיאה");
}
};
const isMetaRunning = metadataTaskId !== null
&& metadataProgress?.status !== "completed"
&& metadataProgress?.status !== "failed";
return (
<Sheet open={open} onOpenChange={(o) => { if (!o) onOpenChange(false); }}>
<SheetContent side="left" className="w-full sm:max-w-2xl overflow-y-auto" dir="rtl">
@@ -150,7 +119,9 @@ export function PrecedentEditSheet({ caseLawId, onOpenChange }: Props) {
<SheetTitle className="text-navy">עריכת פרטי פסיקה</SheetTitle>
<SheetDescription className="text-ink-muted">
כל השדות ניתנים לעריכה חוץ ממראה המקום (מזהה ייחודי).
כפתור &quot;חלץ מטא-דאטה אוטומטית&quot; מנתח את הטקסט וממלא רק שדות ריקים.
לחילוץ מטא-דאטה אוטומטי או הלכות להפעיל מ-Claude Code את
ה-MCP tools <code>precedent_extract_metadata</code> /{" "}
<code>precedent_extract_halachot</code>.
</SheetDescription>
</SheetHeader>
@@ -160,34 +131,13 @@ export function PrecedentEditSheet({ caseLawId, onOpenChange }: Props) {
</div>
) : (
<form onSubmit={onSubmit} className="px-6 pb-6 space-y-4 mt-4">
<div className="rounded-lg border border-rule bg-rule-soft/40 p-3 flex items-start gap-3">
<div className="flex-1">
<div className="text-[0.78rem] text-ink-muted">מראה מקום (לא ניתן לעריכה)</div>
<div className="text-navy font-mono text-sm break-all" dir="ltr">
{record.case_number}
</div>
<div className="rounded-lg border border-rule bg-rule-soft/40 p-3">
<div className="text-[0.78rem] text-ink-muted">מראה מקום (לא ניתן לעריכה)</div>
<div className="text-navy font-mono text-sm break-all" dir="ltr">
{record.case_number}
</div>
<Button
type="button" size="sm" variant="outline"
onClick={onTriggerMetadata}
disabled={isMetaRunning || reextractMeta.isPending}
className="shrink-0"
>
{isMetaRunning ? (
<Loader2 className="w-3.5 h-3.5 me-1 animate-spin" />
) : (
<Sparkles className="w-3.5 h-3.5 me-1" />
)}
חלץ מטא-דאטה אוטומטית
</Button>
</div>
{isMetaRunning && (metadataProgress as { step?: string } | null)?.step && (
<div className="text-[0.78rem] text-ink-muted">
{(metadataProgress as { step?: string }).step}
</div>
)}
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1">
<Label htmlFor="case-name">שם קצר</Label>

View File

@@ -336,34 +336,12 @@ export function useUpdatePrecedent() {
});
}
export function useReExtractHalachot() {
const qc = useQueryClient();
return useMutation({
mutationFn: (id: string) =>
apiRequest<{ task_id: string }>(
`/api/precedent-library/${encodeURIComponent(id)}/extract-halachot`,
{ method: "POST" },
),
onSuccess: (_, id) => {
qc.invalidateQueries({ queryKey: libraryKeys.detail(id) });
},
});
}
export function useReExtractMetadata() {
const qc = useQueryClient();
return useMutation({
mutationFn: (id: string) =>
apiRequest<{ task_id: string }>(
`/api/precedent-library/${encodeURIComponent(id)}/extract-metadata`,
{ method: "POST" },
),
onSuccess: (_, id) => {
qc.invalidateQueries({ queryKey: libraryKeys.detail(id) });
qc.invalidateQueries({ queryKey: libraryKeys.all });
},
});
}
// Halacha + metadata extraction are not exposed as HTTP mutations because
// they call the local `claude` CLI through the MCP server — see the rule
// in mcp-server/src/legal_mcp/services/claude_session.py. The chair
// triggers them from Claude Code via:
// mcp__legal-ai__precedent_extract_halachot <case_law_id>
// mcp__legal-ai__precedent_extract_metadata <case_law_id>
export function useHalachotPending(limit = 200) {
return useQuery({