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,
|
||||
useRunFinalLearning,
|
||||
useRunFinalHalacha,
|
||||
type ExportFile,
|
||||
} from "@/lib/api/exports";
|
||||
import {
|
||||
usePartyClaimsSummary,
|
||||
useGeneratePartyClaimsSummary,
|
||||
useGenerateInterimDraft,
|
||||
} from "@/lib/api/generate";
|
||||
import {
|
||||
useCaseFeedback,
|
||||
useCreateFeedback,
|
||||
@@ -55,6 +61,10 @@ import {
|
||||
Stamp,
|
||||
Link2,
|
||||
AlertTriangle,
|
||||
Zap,
|
||||
ClipboardList,
|
||||
Files,
|
||||
ExternalLink,
|
||||
} from "lucide-react";
|
||||
|
||||
/* Statuses at which a draft is considered ready */
|
||||
@@ -76,6 +86,28 @@ function formatDate(epoch: number): string {
|
||||
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 ─────────────────────────────────── */
|
||||
|
||||
export function DraftsPanel({
|
||||
@@ -97,6 +129,10 @@ export function DraftsPanel({
|
||||
const uploadFinal = useUploadFinalDecision(caseNumber);
|
||||
const runLearning = useRunFinalLearning(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 fileRef = useRef<HTMLInputElement>(null);
|
||||
@@ -129,6 +165,38 @@ export function DraftsPanel({
|
||||
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) {
|
||||
uploadDraft.mutate(file, {
|
||||
onSuccess: (data) => {
|
||||
@@ -233,30 +301,178 @@ export function DraftsPanel({
|
||||
}
|
||||
|
||||
return (
|
||||
// flex + per-card `order` so the visual order matches mockup 18h
|
||||
// (drafts → final+learning → feedback) without reordering the JSX.
|
||||
// flex + per-card `order` so the visual order matches mockup 26:
|
||||
// generate-card → banner → active-draft → drafts → final+learning → feedback.
|
||||
<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 && (
|
||||
<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" />
|
||||
<span className="text-sm font-medium text-gold-deep">
|
||||
{draftLabel}
|
||||
</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>
|
||||
)}
|
||||
|
||||
|
||||
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_argument_aggregation as pc_wake_analyst_for_argument_aggregation,
|
||||
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_curator_for_final as pc_wake_curator_for_final,
|
||||
wake_for_precedent_extraction as pc_wake_for_precedent_extraction,
|
||||
@@ -97,6 +98,7 @@ __all__ = [
|
||||
"pc_get_agents_for_case",
|
||||
"pc_get_agents",
|
||||
"pc_wake_ceo",
|
||||
"pc_wake_ceo_for_action",
|
||||
"pc_wake_ceo_for_feedback_fold",
|
||||
"pc_wake_curator_for_final",
|
||||
"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,
|
||||
rename_case_project,
|
||||
pc_wake_ceo,
|
||||
pc_wake_ceo_for_action,
|
||||
pc_wake_ceo_for_feedback_fold,
|
||||
pc_wake_curator_for_final,
|
||||
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")}
|
||||
|
||||
|
||||
# ── 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")
|
||||
async def api_party_claims_summary_download(case_number: str):
|
||||
"""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
|
||||
|
||||
|
||||
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]:
|
||||
"""Build the (sub-issue title, description) for a staged final-decision task.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user