diff --git a/web-ui/src/components/cases/agent-status-widget.tsx b/web-ui/src/components/cases/agent-status-widget.tsx index 86f4ee8..0171521 100644 --- a/web-ui/src/components/cases/agent-status-widget.tsx +++ b/web-ui/src/components/cases/agent-status-widget.tsx @@ -1,8 +1,20 @@ "use client"; -import { useAgentActivity } from "@/lib/api/agents"; +import { useState } from "react"; +import { useAgentActivity, useResetCaseAgents } from "@/lib/api/agents"; import type { PaperclipAgentStatus } from "@/lib/api/agents"; -import { Bot } from "lucide-react"; +import { Bot, RotateCcw, Loader2 } from "lucide-react"; +import { toast } from "sonner"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; /* ── Status dot colors ───────────────────────────────────────── */ @@ -40,6 +52,75 @@ function AgentRow({ agent }: { agent: PaperclipAgentStatus }) { ); } +/* ── Reset button ─────────────────────────────────────────────── */ + +function ResetAgentsButton({ caseNumber }: { caseNumber: string }) { + const [open, setOpen] = useState(false); + const reset = useResetCaseAgents(caseNumber); + + function handleConfirm() { + reset.mutate(undefined, { + onSuccess: (result) => { + setOpen(false); + const agentNames = result.reset_agents + .filter((a) => a.ok) + .map((a) => a.name) + .join(", "); + const msg = agentNames + ? `סוכנים אופסו: ${agentNames}` + : "אופסו בהצלחה"; + toast.success(msg); + }, + onError: () => { + setOpen(false); + toast.error("שגיאה בביצוע האיפוס"); + }, + }); + } + + return ( + + + + + + + איפוס סוכנים + + פעולה זו תאפס את כל הסוכנים במצב שגיאה ותחזיר issues פתוחים לניהול ידני. + פעולה הפיכה — ניתן להפעיל את הסוכנים מחדש אחרי האיפוס. + + + + + + + + + ); +} + /* ── Widget ───────────────────────────────────────────────────── */ export function AgentStatusWidget({ @@ -54,6 +135,7 @@ export function AgentStatusWidget({ const agents = data.agents ?? []; const activeCount = agents.filter((a) => a.status === "active" || a.status === "running").length; + const hasErrors = agents.some((a) => a.status === "error"); return (
@@ -62,11 +144,14 @@ export function AgentStatusWidget({ סוכנים
- {agents.length > 0 && ( - - {activeCount} פעילים מתוך {agents.length} - - )} +
+ {hasErrors && } + {agents.length > 0 && ( + + {activeCount} פעילים מתוך {agents.length} + + )} +
diff --git a/web-ui/src/lib/api/agents.ts b/web-ui/src/lib/api/agents.ts index f749ffb..4079ebe 100644 --- a/web-ui/src/lib/api/agents.ts +++ b/web-ui/src/lib/api/agents.ts @@ -179,3 +179,25 @@ export function useSubmitInteraction(caseNumber: string | undefined) { }, }); } + +export type AgentResetResult = { + ok: boolean; + reassigned_issues: { id: string; identifier: string }[]; + reset_agents: { id: string; name: string; ok: boolean; error?: string }[]; +}; + +export function useResetCaseAgents(caseNumber: string | undefined) { + const qc = useQueryClient(); + return useMutation({ + mutationFn: () => + apiRequest( + `/api/cases/${caseNumber}/agents/reset`, + { method: "POST" }, + ), + onSuccess: () => { + if (caseNumber) { + qc.invalidateQueries({ queryKey: agentKeys.activity(caseNumber) }); + } + }, + }); +} diff --git a/web/agent_platform_port.py b/web/agent_platform_port.py index 4af9c16..5f65f05 100644 --- a/web/agent_platform_port.py +++ b/web/agent_platform_port.py @@ -52,6 +52,7 @@ from web.paperclip_client import ( post_comment as pc_post_comment, reject_interaction as pc_reject_interaction, reset_agent_session as pc_reset_agent_session, + reset_case_agents as pc_reset_case_agents, respond_to_interaction as pc_respond_to_interaction, restore_project as pc_restore_project, update_project_name as pc_update_project_name, @@ -114,4 +115,5 @@ __all__ = [ "pc_get_run_events", "pc_cancel_run", "pc_reset_agent_session", + "pc_reset_case_agents", ] diff --git a/web/app.py b/web/app.py index 2d45b6f..a1b141a 100644 --- a/web/app.py +++ b/web/app.py @@ -72,6 +72,7 @@ from web.agent_platform_port import ( pc_reject_interaction, pc_request, pc_reset_agent_session, + pc_reset_case_agents, pc_respond_to_interaction, pc_restore_project, pc_wake_analyst_for_appraiser_facts, @@ -4173,6 +4174,19 @@ async def api_post_interaction_response( raise HTTPException(502, f"שגיאת Paperclip: {e}") +@app.post("/api/cases/{case_number}/agents/reset") +async def api_reset_case_agents(case_number: str): + """Reset stuck agents for a case. + + Clears writer/QA agents from 'error' status and reassigns any open + issues back to the chair user, stopping Paperclip recovery loops. + """ + result = await pc_reset_case_agents(case_number) + if not result.get("ok"): + raise HTTPException(502, result.get("error", "שגיאה בביצוע האיפוס")) + return result + + # ── Settings: MCP Server Configuration ──────────────────────────── # # Source of truth for legal-ai env vars is Coolify (see memory: diff --git a/web/paperclip_client.py b/web/paperclip_client.py index 0186318..cbe83a8 100644 --- a/web/paperclip_client.py +++ b/web/paperclip_client.py @@ -791,6 +791,71 @@ async def reset_agent_session(agent_id: str) -> dict: return resp.json() +CHAIM_USER_ID = "ZpDWXxFweC3MftuF1Ttyu2VUbSPHbKZd" + + +async def reset_case_agents(case_number: str) -> dict: + """Reset agent state for a case: clear error status + reassign stuck issues to user. + + Two actions: + 1. Any non-completed issue still assigned to an agent is reassigned to the chair user, + stopping Paperclip's source_scoped_recovery_action loop. + 2. Every agent in the case's company whose global status is 'error' gets a + reset_agent_session call (clears wedged runtime) and its DB status set to 'idle'. + """ + first_digit = case_number.split("-")[0][0] if case_number else "1" + company_id = COMPANIES["betterment"] if first_digit in ("8", "9") else COMPANIES["licensing"] + + conn = await asyncpg.connect(PAPERCLIP_DB_URL) + try: + project = await conn.fetchrow( + "SELECT id FROM projects WHERE name LIKE $1 LIMIT 1", + f"%{case_number}%", + ) + if not project: + return {"ok": False, "error": f"No Paperclip project found for {case_number}"} + + project_id = project["id"] + + reassigned = await conn.fetch( + """UPDATE issues + SET assignee_agent_id = null, assignee_user_id = $1, updated_at = now() + WHERE project_id = $2 + AND assignee_agent_id IS NOT NULL + AND status NOT IN ('done', 'cancelled') + RETURNING id, identifier""", + CHAIM_USER_ID, project_id, + ) + + error_agents = await conn.fetch( + "SELECT id, name FROM agents WHERE company_id = $1::uuid AND status = 'error'", + company_id, + ) + + if error_agents: + await conn.execute( + "UPDATE agents SET status = 'idle' WHERE id = ANY($1::uuid[])", + [r["id"] for r in error_agents], + ) + finally: + await conn.close() + + reset_results = [] + for agent in error_agents: + try: + await reset_agent_session(str(agent["id"])) + reset_results.append({"id": str(agent["id"]), "name": agent["name"], "ok": True}) + except Exception as e: + logger.warning("reset_agent_session failed for %s: %s", agent["id"], e) + reset_results.append({"id": str(agent["id"]), "name": agent["name"], "ok": False, "error": str(e)}) + + return { + "ok": True, + "reassigned_issues": [{"id": str(r["id"]), "identifier": r["identifier"]} for r in reassigned], + "reset_agents": reset_results, + } + + async def respond_to_interaction( issue_id: str, interaction_id: str, payload: dict, ) -> dict: