Merge pull request 'feat(case-ui): כרטיס "הפקת מסמכים" עם 3 כפתורי-הפק דטרמיניסטיים (#214)' (#386) from worktree-agent-a72c8793960b71772 into main
This commit was merged in pull request #386.
This commit is contained in:
@@ -25,7 +25,13 @@ import {
|
|||||||
useUploadFinalDecision,
|
useUploadFinalDecision,
|
||||||
useRunFinalLearning,
|
useRunFinalLearning,
|
||||||
useRunFinalHalacha,
|
useRunFinalHalacha,
|
||||||
|
type ExportFile,
|
||||||
} from "@/lib/api/exports";
|
} from "@/lib/api/exports";
|
||||||
|
import {
|
||||||
|
usePartyClaimsSummary,
|
||||||
|
useGeneratePartyClaimsSummary,
|
||||||
|
useGenerateInterimDraft,
|
||||||
|
} from "@/lib/api/generate";
|
||||||
import {
|
import {
|
||||||
useCaseFeedback,
|
useCaseFeedback,
|
||||||
useCreateFeedback,
|
useCreateFeedback,
|
||||||
@@ -55,6 +61,10 @@ import {
|
|||||||
Stamp,
|
Stamp,
|
||||||
Link2,
|
Link2,
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
|
Zap,
|
||||||
|
ClipboardList,
|
||||||
|
Files,
|
||||||
|
ExternalLink,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
/* Statuses at which a draft is considered ready */
|
/* Statuses at which a draft is considered ready */
|
||||||
@@ -76,6 +86,28 @@ function formatDate(epoch: number): string {
|
|||||||
return formatDateTime(epoch * 1000);
|
return formatDateTime(epoch * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pick the newest export file whose name starts with `prefix`, preferring the
|
||||||
|
* highest v-number and falling back to the latest created_at. Used by the
|
||||||
|
* "הפקת מסמכים" card to surface the most recent טיוטת-ביניים-* file (#214).
|
||||||
|
*/
|
||||||
|
function pickLatestVersioned(
|
||||||
|
files: ExportFile[] | undefined,
|
||||||
|
prefix: string,
|
||||||
|
): (ExportFile & { version: number | null }) | null {
|
||||||
|
const matches = (files ?? []).filter((f) => f.filename.startsWith(prefix));
|
||||||
|
if (!matches.length) return null;
|
||||||
|
const withVer = matches.map((f) => {
|
||||||
|
const m = f.filename.match(/v(\d+)/);
|
||||||
|
return { ...f, version: m ? parseInt(m[1], 10) : null };
|
||||||
|
});
|
||||||
|
withVer.sort((a, b) => {
|
||||||
|
if (a.version != null && b.version != null) return b.version - a.version;
|
||||||
|
return b.created_at - a.created_at;
|
||||||
|
});
|
||||||
|
return withVer[0];
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Main component ─────────────────────────────────── */
|
/* ── Main component ─────────────────────────────────── */
|
||||||
|
|
||||||
export function DraftsPanel({
|
export function DraftsPanel({
|
||||||
@@ -97,6 +129,10 @@ export function DraftsPanel({
|
|||||||
const uploadFinal = useUploadFinalDecision(caseNumber);
|
const uploadFinal = useUploadFinalDecision(caseNumber);
|
||||||
const runLearning = useRunFinalLearning(caseNumber);
|
const runLearning = useRunFinalLearning(caseNumber);
|
||||||
const runHalacha = useRunFinalHalacha(caseNumber);
|
const runHalacha = useRunFinalHalacha(caseNumber);
|
||||||
|
// ── Document generation (#214) — the "הפקת מסמכים" card ──
|
||||||
|
const { data: summary } = usePartyClaimsSummary(caseNumber);
|
||||||
|
const genSummary = useGeneratePartyClaimsSummary(caseNumber);
|
||||||
|
const genInterim = useGenerateInterimDraft(caseNumber);
|
||||||
const qc = useQueryClient();
|
const qc = useQueryClient();
|
||||||
|
|
||||||
const fileRef = useRef<HTMLInputElement>(null);
|
const fileRef = useRef<HTMLInputElement>(null);
|
||||||
@@ -129,6 +165,38 @@ export function DraftsPanel({
|
|||||||
return `טיוטה v${maxVer}${suffix} מוכנה לעיון`;
|
return `טיוטה v${maxVer}${suffix} מוכנה לעיון`;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
// ── "הפקת מסמכים" derived state (#214) ──
|
||||||
|
// Latest interim ("party-claims") partial draft in the exports table — the poll
|
||||||
|
// target for button 2. The CEO writes it as טיוטת-ביניים-{case}-vN.docx.
|
||||||
|
const latestInterim = pickLatestVersioned(exports, "טיוטת-ביניים-");
|
||||||
|
const summaryReady = Boolean(summary?.markdown);
|
||||||
|
|
||||||
|
function handleGenerateSummary() {
|
||||||
|
genSummary.mutate(undefined, {
|
||||||
|
onSuccess: (d) => {
|
||||||
|
if (d.status === "ok") {
|
||||||
|
toast.success("הופעלה הפקת סיכום-המנהלים — רצה ברקע (אופוס)");
|
||||||
|
} else {
|
||||||
|
toast.warning(`לא הופעלה ההפקה: ${d.reason ?? d.error ?? d.status}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: () => toast.error("שגיאה בהפעלת הפקת סיכום-המנהלים"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleGenerateInterim() {
|
||||||
|
genInterim.mutate(undefined, {
|
||||||
|
onSuccess: (d) => {
|
||||||
|
if (d.status === "ok") {
|
||||||
|
toast.success("הופעלה הפקת טיוטת טענות-הצדדים — רצה ברקע (אופוס)");
|
||||||
|
} else {
|
||||||
|
toast.warning(`לא הופעלה ההפקה: ${d.reason ?? d.error ?? d.status}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: () => toast.error("שגיאה בהפעלת הפקת הטיוטה החלקית"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function handleUpload(file: File) {
|
function handleUpload(file: File) {
|
||||||
uploadDraft.mutate(file, {
|
uploadDraft.mutate(file, {
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
@@ -233,30 +301,178 @@ export function DraftsPanel({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// flex + per-card `order` so the visual order matches mockup 18h
|
// flex + per-card `order` so the visual order matches mockup 26:
|
||||||
// (drafts → final+learning → feedback) without reordering the JSX.
|
// generate-card → banner → active-draft → drafts → final+learning → feedback.
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
{/* ── Banner ── */}
|
{/* ── Card: document generation (mockup 26) — 3 deterministic "הפק" buttons ── */}
|
||||||
|
<section className="order-0 rounded-lg border border-rule bg-surface overflow-hidden">
|
||||||
|
<div className="flex items-center gap-2.5 px-4 py-3 border-b border-rule-soft bg-parchment">
|
||||||
|
<Zap className="w-[18px] h-[18px] text-gold-deep shrink-0" />
|
||||||
|
<h3 className="text-navy text-base font-semibold">הפקת מסמכים</h3>
|
||||||
|
</div>
|
||||||
|
<div className="p-4">
|
||||||
|
<p className="text-xs text-ink-muted leading-relaxed mb-1">
|
||||||
|
הפקה דטרמיניסטית בלחיצה — בלי בקשת-טקסט. כל כפתור מריץ פעולה מדויקת,
|
||||||
|
והקובץ מופיע בטבלת-הטיוטות / לקריאה מקומית.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* 1 — executive summary of party claims */}
|
||||||
|
<div className="flex items-start gap-3 py-3 border-t border-rule-soft first-of-type:border-t-0">
|
||||||
|
<span className="flex w-9 h-9 shrink-0 items-center justify-center rounded-lg border border-rule bg-gold-wash text-gold-deep">
|
||||||
|
<ClipboardList className="w-[18px] h-[18px]" />
|
||||||
|
</span>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="text-sm font-semibold text-navy">הפק סיכום מנהלים</div>
|
||||||
|
<p className="text-xs text-ink-muted mt-0.5 leading-relaxed">
|
||||||
|
תמצית מזוקקת של טענות הצדדים לקראת הדיון — מסמך נפרד מהטיוטה.
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap items-center gap-2.5 mt-2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={handleGenerateSummary}
|
||||||
|
disabled={genSummary.isPending}
|
||||||
|
className="bg-navy hover:bg-navy-soft text-parchment"
|
||||||
|
>
|
||||||
|
{genSummary.isPending ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="w-3.5 h-3.5 animate-spin me-1.5" />
|
||||||
|
מייצר…
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"הפק"
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
{summaryReady && (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-7 px-2 text-gold-deep"
|
||||||
|
onClick={() =>
|
||||||
|
window.open(
|
||||||
|
`/api/cases/${caseNumber}/research/party-claims-summary/download`,
|
||||||
|
"_blank",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ExternalLink className="w-3.5 h-3.5 me-1" />
|
||||||
|
פתח
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-7 px-2 text-gold-deep"
|
||||||
|
onClick={() =>
|
||||||
|
window.open(
|
||||||
|
`/api/cases/${caseNumber}/research/party-claims-summary/export-docx`,
|
||||||
|
"_blank",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Download className="w-3.5 h-3.5 me-1" />
|
||||||
|
DOCX
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 2 — partial draft (parties' claims, blocks ה–ט) */}
|
||||||
|
<div className="flex items-start gap-3 py-3 border-t border-rule-soft">
|
||||||
|
<span className="flex w-9 h-9 shrink-0 items-center justify-center rounded-lg border border-rule bg-gold-wash text-gold-deep">
|
||||||
|
<FileText className="w-[18px] h-[18px]" />
|
||||||
|
</span>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="text-sm font-semibold text-navy">
|
||||||
|
הפק טיוטה של טענות הצדדים{" "}
|
||||||
|
<span className="font-medium text-ink-muted text-xs">
|
||||||
|
(טיוטה חלקית)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-ink-muted mt-0.5 leading-relaxed">
|
||||||
|
בלוקים ה–ט (רקע, טענות, הליכים, תכניות) — עד הדיון, בלי דיון
|
||||||
|
והכרעה. נשמרת בטבלת-הטיוטות.
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap items-center gap-2.5 mt-2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={handleGenerateInterim}
|
||||||
|
disabled={genInterim.isPending}
|
||||||
|
className="bg-navy hover:bg-navy-soft text-parchment"
|
||||||
|
>
|
||||||
|
{genInterim.isPending ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="w-3.5 h-3.5 animate-spin me-1.5" />
|
||||||
|
מייצר…
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"הפק"
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
{latestInterim && (
|
||||||
|
<span className="text-[0.7rem] text-ink-muted">
|
||||||
|
{latestInterim.version
|
||||||
|
? `v${latestInterim.version} · `
|
||||||
|
: ""}
|
||||||
|
{formatDate(latestInterim.created_at)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 3 — full decision draft (REUSES useExportDocx; moved from the banner) */}
|
||||||
|
<div className="flex items-start gap-3 py-3 border-t border-rule-soft">
|
||||||
|
<span className="flex w-9 h-9 shrink-0 items-center justify-center rounded-lg border border-rule bg-gold-wash text-gold-deep">
|
||||||
|
<Files className="w-[18px] h-[18px]" />
|
||||||
|
</span>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="text-sm font-semibold text-navy">
|
||||||
|
הפק טיוטת החלטה מלאה
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-ink-muted mt-0.5 leading-relaxed">
|
||||||
|
ההחלטה המלאה — כל 12 הבלוקים, כולל דיון והכרעה. נקבעת
|
||||||
|
כמקור-האמת ונשמרת בטבלת-הטיוטות.
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap items-center gap-2.5 mt-2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={handleExport}
|
||||||
|
disabled={exportDocx.isPending || !isDraftReady}
|
||||||
|
title={!isDraftReady ? "זמין כשיש בלוקים כתובים" : undefined}
|
||||||
|
className="bg-navy hover:bg-navy-soft text-parchment"
|
||||||
|
>
|
||||||
|
{exportDocx.isPending ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="w-3.5 h-3.5 animate-spin me-1.5" />
|
||||||
|
מייצר…
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<FileOutput className="w-3.5 h-3.5 me-1.5" />
|
||||||
|
הפק
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
{!isDraftReady && (
|
||||||
|
<span className="rounded bg-warn-bg px-2 py-0.5 text-[0.65rem] text-warn">
|
||||||
|
זמין כשיש בלוקים כתובים
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* ── Banner — draft-ready indicator (export button moved to "הפקת מסמכים") ── */}
|
||||||
{isDraftReady && (
|
{isDraftReady && (
|
||||||
<div className="order-1 flex items-center gap-3 rounded-lg border border-gold/40 bg-gold-wash px-4 py-3">
|
<div className="order-1 flex items-center gap-3 rounded-lg border border-gold/40 bg-gold-wash px-4 py-3">
|
||||||
<FileText className="w-5 h-5 text-gold-deep shrink-0" />
|
<FileText className="w-5 h-5 text-gold-deep shrink-0" />
|
||||||
<span className="text-sm font-medium text-gold-deep">
|
<span className="text-sm font-medium text-gold-deep">
|
||||||
{draftLabel}
|
{draftLabel}
|
||||||
</span>
|
</span>
|
||||||
<div className="me-auto" />
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
onClick={handleExport}
|
|
||||||
disabled={exportDocx.isPending}
|
|
||||||
className="bg-navy hover:bg-navy-soft text-parchment"
|
|
||||||
>
|
|
||||||
{exportDocx.isPending ? (
|
|
||||||
<Loader2 className="w-4 h-4 animate-spin me-1.5" />
|
|
||||||
) : (
|
|
||||||
<FileOutput className="w-4 h-4 me-1.5" />
|
|
||||||
)}
|
|
||||||
הפק DOCX
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
92
web-ui/src/lib/api/generate.ts
Normal file
92
web-ui/src/lib/api/generate.ts
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
/**
|
||||||
|
* Document-generation domain hooks (TaskMaster #214).
|
||||||
|
*
|
||||||
|
* The "הפקת מסמכים" card in the case "טיוטות והערות" tab fires two deterministic,
|
||||||
|
* host-side generation actions through the CEO (structured-action wakeup):
|
||||||
|
* • party_claims_summary → an executive summary of the parties' claims
|
||||||
|
* • interim_draft → a partial draft (blocks ה–ט, up to the discussion)
|
||||||
|
*
|
||||||
|
* Generation runs on the host (claude_session → claude -p), NOT in the container,
|
||||||
|
* so the trigger endpoints return 202-accepted and the UI POLLS for completion:
|
||||||
|
* • summary → GET /api/cases/{n}/research/party-claims-summary (200 = ready)
|
||||||
|
* • interim draft → the exports list grows a "טיוטת-ביניים-{n}-vN.docx" file
|
||||||
|
* (polled via useExports — see exports.ts).
|
||||||
|
*
|
||||||
|
* The full-decision export ("הפק טיוטת החלטה מלאה") is NOT here — it reuses the
|
||||||
|
* existing in-container useExportDocx (exports.ts), no LLM, no wakeup.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { ApiError, apiRequest } from "./client";
|
||||||
|
|
||||||
|
/** Shape returned by the structured-action wakeup endpoints (#214). */
|
||||||
|
export type GenerateTriggerResult = {
|
||||||
|
status: "ok" | "skipped" | "error";
|
||||||
|
action?: string;
|
||||||
|
issue_id?: string;
|
||||||
|
ceo_id?: string;
|
||||||
|
reason?: string;
|
||||||
|
error?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generateKeys = {
|
||||||
|
all: ["generate"] as const,
|
||||||
|
partyClaimsSummary: (caseNumber: string) =>
|
||||||
|
[...generateKeys.all, "party-claims-summary", caseNumber] as const,
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ── Read: party-claims executive summary (poll target for button 1) ──
|
||||||
|
404 (not generated yet) is a normal "not ready" state, not an error — we
|
||||||
|
coerce it to null so the panel renders the empty/idle state cleanly. */
|
||||||
|
export type PartyClaimsSummary = { markdown: string } | null;
|
||||||
|
|
||||||
|
export function usePartyClaimsSummary(caseNumber: string | undefined) {
|
||||||
|
return useQuery<PartyClaimsSummary>({
|
||||||
|
queryKey: generateKeys.partyClaimsSummary(caseNumber ?? ""),
|
||||||
|
queryFn: async ({ signal }) => {
|
||||||
|
try {
|
||||||
|
return await apiRequest<{ markdown: string }>(
|
||||||
|
`/api/cases/${caseNumber}/research/party-claims-summary`,
|
||||||
|
{ signal },
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof ApiError && e.status === 404) return null;
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
enabled: Boolean(caseNumber),
|
||||||
|
staleTime: 5_000,
|
||||||
|
retry: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Trigger: party-claims executive summary (button 1) ── */
|
||||||
|
export function useGeneratePartyClaimsSummary(caseNumber: string) {
|
||||||
|
const qc = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: () =>
|
||||||
|
apiRequest<GenerateTriggerResult>(
|
||||||
|
`/api/cases/${caseNumber}/generate/party-claims-summary`,
|
||||||
|
{ method: "POST" },
|
||||||
|
),
|
||||||
|
onSuccess: () => {
|
||||||
|
// Start polling fresh — the file will appear after the host run completes.
|
||||||
|
qc.invalidateQueries({
|
||||||
|
queryKey: generateKeys.partyClaimsSummary(caseNumber),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Trigger: interim ("party-claims") partial draft (button 2) ── */
|
||||||
|
export function useGenerateInterimDraft(caseNumber: string) {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: () =>
|
||||||
|
apiRequest<GenerateTriggerResult>(
|
||||||
|
`/api/cases/${caseNumber}/generate/interim-draft`,
|
||||||
|
{ method: "POST" },
|
||||||
|
),
|
||||||
|
// The result lands in the exports list (טיוטת-ביניים-*.docx); useExports
|
||||||
|
// already polls on a 5s interval, so no extra invalidation is needed here.
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -59,6 +59,7 @@ from web.paperclip_client import (
|
|||||||
wake_analyst_for_appraiser_facts as pc_wake_analyst_for_appraiser_facts,
|
wake_analyst_for_appraiser_facts as pc_wake_analyst_for_appraiser_facts,
|
||||||
wake_analyst_for_argument_aggregation as pc_wake_analyst_for_argument_aggregation,
|
wake_analyst_for_argument_aggregation as pc_wake_analyst_for_argument_aggregation,
|
||||||
wake_ceo_agent as pc_wake_ceo,
|
wake_ceo_agent as pc_wake_ceo,
|
||||||
|
wake_ceo_for_action as pc_wake_ceo_for_action,
|
||||||
wake_ceo_for_feedback_fold as pc_wake_ceo_for_feedback_fold,
|
wake_ceo_for_feedback_fold as pc_wake_ceo_for_feedback_fold,
|
||||||
wake_curator_for_final as pc_wake_curator_for_final,
|
wake_curator_for_final as pc_wake_curator_for_final,
|
||||||
wake_for_precedent_extraction as pc_wake_for_precedent_extraction,
|
wake_for_precedent_extraction as pc_wake_for_precedent_extraction,
|
||||||
@@ -97,6 +98,7 @@ __all__ = [
|
|||||||
"pc_get_agents_for_case",
|
"pc_get_agents_for_case",
|
||||||
"pc_get_agents",
|
"pc_get_agents",
|
||||||
"pc_wake_ceo",
|
"pc_wake_ceo",
|
||||||
|
"pc_wake_ceo_for_action",
|
||||||
"pc_wake_ceo_for_feedback_fold",
|
"pc_wake_ceo_for_feedback_fold",
|
||||||
"pc_wake_curator_for_final",
|
"pc_wake_curator_for_final",
|
||||||
"pc_wake_for_precedent_extraction",
|
"pc_wake_for_precedent_extraction",
|
||||||
|
|||||||
46
web/app.py
46
web/app.py
@@ -79,6 +79,7 @@ from web.agent_platform_port import (
|
|||||||
pc_wake_analyst_for_argument_aggregation,
|
pc_wake_analyst_for_argument_aggregation,
|
||||||
rename_case_project,
|
rename_case_project,
|
||||||
pc_wake_ceo,
|
pc_wake_ceo,
|
||||||
|
pc_wake_ceo_for_action,
|
||||||
pc_wake_ceo_for_feedback_fold,
|
pc_wake_ceo_for_feedback_fold,
|
||||||
pc_wake_curator_for_final,
|
pc_wake_curator_for_final,
|
||||||
pc_wake_for_precedent_extraction,
|
pc_wake_for_precedent_extraction,
|
||||||
@@ -3090,6 +3091,51 @@ async def api_party_claims_summary(case_number: str):
|
|||||||
return {"markdown": path.read_text(encoding="utf-8")}
|
return {"markdown": path.read_text(encoding="utf-8")}
|
||||||
|
|
||||||
|
|
||||||
|
# ── Deterministic document generation triggers (TaskMaster #214) ─────
|
||||||
|
# The "הפקת מסמכים" buttons in the case "טיוטות והערות" tab. Generation for the
|
||||||
|
# executive summary + interim draft runs HOST-SIDE (claude_session → claude -p,
|
||||||
|
# not in the container), so these endpoints fire a *structured-action* CEO wakeup
|
||||||
|
# (the CEO reads payload.action and routes to שלב H2 / שלב H — legal-ceo.md).
|
||||||
|
# Wakeup goes through the Paperclip API helper (pc_wake_ceo_for_action → the
|
||||||
|
# platform Port → POST /api/agents/{id}/wakeup), NEVER a direct DB insert.
|
||||||
|
# Polling for completion uses the existing read endpoints (research/party-claims-
|
||||||
|
# summary for the summary; the exports list for the טיוטת-ביניים-*.docx file).
|
||||||
|
|
||||||
|
|
||||||
|
async def _wake_ceo_action(case_number: str, action: str) -> dict:
|
||||||
|
"""Resolve the case's company + fire a structured-action CEO wakeup (#214)."""
|
||||||
|
case = await db.get_case_by_number(case_number)
|
||||||
|
if not case:
|
||||||
|
raise HTTPException(404, f"תיק {case_number} לא נמצא")
|
||||||
|
prefix = case_number[:1]
|
||||||
|
company_id = (
|
||||||
|
PAPERCLIP_COMPANIES["licensing"] if prefix == "1"
|
||||||
|
else PAPERCLIP_COMPANIES["betterment"] if prefix in ("8", "9")
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
return await pc_wake_ceo_for_action(case_number, action, company_id=company_id)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("CEO action %s wakeup failed for %s: %s", action, case_number, e)
|
||||||
|
return {"status": "error", "error": str(e)}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/cases/{case_number}/generate/party-claims-summary", status_code=202)
|
||||||
|
async def api_generate_party_claims_summary(case_number: str):
|
||||||
|
"""Fire-and-accept: wake the CEO to generate the party-claims executive summary
|
||||||
|
(שלב H2 → summarize_party_claims). Runs host-side; poll
|
||||||
|
GET /api/cases/{n}/research/party-claims-summary for completion."""
|
||||||
|
return await _wake_ceo_action(case_number, "party_claims_summary")
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/cases/{case_number}/generate/interim-draft", status_code=202)
|
||||||
|
async def api_generate_interim_draft(case_number: str):
|
||||||
|
"""Fire-and-accept: wake the CEO to generate the partial "party-claims" draft
|
||||||
|
(שלב H → write_interim_draft + export_interim_draft). Runs host-side; poll the
|
||||||
|
exports list for the new טיוטת-ביניים-{case}-vN.docx."""
|
||||||
|
return await _wake_ceo_action(case_number, "interim_draft")
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/cases/{case_number}/research/party-claims-summary/download")
|
@app.get("/api/cases/{case_number}/research/party-claims-summary/download")
|
||||||
async def api_party_claims_summary_download(case_number: str):
|
async def api_party_claims_summary_download(case_number: str):
|
||||||
"""Download the raw party-claims-summary.md file."""
|
"""Download the raw party-claims-summary.md file."""
|
||||||
|
|||||||
@@ -1195,6 +1195,73 @@ async def wake_ceo_agent(issue_id: str, case_number: str, company_id: str = "")
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
async def wake_ceo_for_action(
|
||||||
|
case_number: str,
|
||||||
|
action: str,
|
||||||
|
company_id: str = "",
|
||||||
|
) -> dict:
|
||||||
|
"""Wake the CEO with a deterministic *structured action* on the case's MAIN issue.
|
||||||
|
|
||||||
|
Drives the UI "הפקת מסמכים" buttons (TaskMaster #214): the CEO reads
|
||||||
|
``payload.action`` and routes deterministically to its שלב H / שלב H2
|
||||||
|
side-quests (legal-ceo.md §"פעולות סטרוקטורליות") — no free-text parsing.
|
||||||
|
|
||||||
|
action="party_claims_summary" → שלב H2 (summarize_party_claims)
|
||||||
|
action="interim_draft" → שלב H (write_interim_draft + export_interim_draft)
|
||||||
|
|
||||||
|
These are side-quests on the EXISTING ``[ערר {case_number}]`` issue — no child
|
||||||
|
issue is created (the CEO must not change ``cases.status`` or spawn sub-agents).
|
||||||
|
Wakeup goes through the Paperclip API (``POST /api/agents/{id}/wakeup``), never a
|
||||||
|
direct DB insert (CLAUDE.md hard rule). Returns
|
||||||
|
``{"status": "ok"|"skipped", ...}``.
|
||||||
|
"""
|
||||||
|
if not PAPERCLIP_BOARD_API_KEY:
|
||||||
|
logger.warning("PAPERCLIP_BOARD_API_KEY not set — skipping CEO action wakeup")
|
||||||
|
return {"status": "skipped", "reason": "no_api_key"}
|
||||||
|
|
||||||
|
issues = await get_case_issues(case_number)
|
||||||
|
if not issues:
|
||||||
|
logger.warning("No Paperclip issues found for case %s — skipping CEO action", case_number)
|
||||||
|
return {"status": "skipped", "reason": "no_issue"}
|
||||||
|
|
||||||
|
# The main case issue — prefer the in-progress one, else the canonical
|
||||||
|
# "[ערר {case_number}]" issue, else the oldest. (Same selection spirit as
|
||||||
|
# wake_curator_for_final, but we never spawn a child — it's a side-quest.)
|
||||||
|
main_issue = (
|
||||||
|
next((i for i in issues if i.get("status") == "in_progress"), None)
|
||||||
|
or next((i for i in issues if f"[ערר {case_number}]" in (i.get("title") or "")), None)
|
||||||
|
or issues[0]
|
||||||
|
)
|
||||||
|
main_issue_id = main_issue["id"]
|
||||||
|
|
||||||
|
ceo_id = CEO_AGENTS.get(company_id, CEO_AGENT_ID)
|
||||||
|
wake_resp = await pc_request(
|
||||||
|
"POST",
|
||||||
|
f"/api/agents/{ceo_id}/wakeup",
|
||||||
|
json={
|
||||||
|
"source": "on_demand",
|
||||||
|
"triggerDetail": "manual",
|
||||||
|
"reason": f"generate_{action}_{case_number}",
|
||||||
|
"payload": {
|
||||||
|
"issueId": main_issue_id,
|
||||||
|
"action": action,
|
||||||
|
"case_number": case_number,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
raise_on_error=True,
|
||||||
|
)
|
||||||
|
logger.info(
|
||||||
|
"CEO action wakeup for case %s: action=%s issue=%s ceo=%s status=%s",
|
||||||
|
case_number, action, main_issue_id, ceo_id, wake_resp.status_code,
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"status": "ok",
|
||||||
|
"action": action,
|
||||||
|
"issue_id": main_issue_id,
|
||||||
|
"ceo_id": ceo_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _curator_task_brief(task: str, case_number: str, final_filename: str) -> tuple[str, str]:
|
def _curator_task_brief(task: str, case_number: str, final_filename: str) -> tuple[str, str]:
|
||||||
"""Build the (sub-issue title, description) for a staged final-decision task.
|
"""Build the (sub-issue title, description) for a staged final-decision task.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user