feat(agents): reset button + fix comment routing fallback #331
@@ -1,8 +1,20 @@
|
|||||||
"use client";
|
"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 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 ───────────────────────────────────────── */
|
/* ── 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 (
|
||||||
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<button
|
||||||
|
className="text-[10px] text-ink-faint hover:text-red-600 flex items-center gap-0.5 transition-colors"
|
||||||
|
title="אפס סוכנים תקועים"
|
||||||
|
>
|
||||||
|
<RotateCcw className="w-3 h-3" />
|
||||||
|
<span>אפס</span>
|
||||||
|
</button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>איפוס סוכנים</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
פעולה זו תאפס את כל הסוכנים במצב שגיאה ותחזיר issues פתוחים לניהול ידני.
|
||||||
|
פעולה הפיכה — ניתן להפעיל את הסוכנים מחדש אחרי האיפוס.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setOpen(false)} disabled={reset.isPending}>
|
||||||
|
ביטול
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
onClick={handleConfirm}
|
||||||
|
disabled={reset.isPending}
|
||||||
|
>
|
||||||
|
{reset.isPending ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="w-3.5 h-3.5 ms-1.5 animate-spin" />
|
||||||
|
מאפס...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"אפס סוכנים"
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Widget ───────────────────────────────────────────────────── */
|
/* ── Widget ───────────────────────────────────────────────────── */
|
||||||
|
|
||||||
export function AgentStatusWidget({
|
export function AgentStatusWidget({
|
||||||
@@ -54,6 +135,7 @@ export function AgentStatusWidget({
|
|||||||
|
|
||||||
const agents = data.agents ?? [];
|
const agents = data.agents ?? [];
|
||||||
const activeCount = agents.filter((a) => a.status === "active" || a.status === "running").length;
|
const activeCount = agents.filter((a) => a.status === "active" || a.status === "running").length;
|
||||||
|
const hasErrors = agents.some((a) => a.status === "error");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@@ -62,12 +144,15 @@ export function AgentStatusWidget({
|
|||||||
<Bot className="w-3.5 h-3.5" />
|
<Bot className="w-3.5 h-3.5" />
|
||||||
<span>סוכנים</span>
|
<span>סוכנים</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{hasErrors && <ResetAgentsButton caseNumber={caseNumber} />}
|
||||||
{agents.length > 0 && (
|
{agents.length > 0 && (
|
||||||
<span className="text-[10px] text-ink-faint">
|
<span className="text-[10px] text-ink-faint">
|
||||||
{activeCount} פעילים מתוך {agents.length}
|
{activeCount} פעילים מתוך {agents.length}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
{agents.map((agent) => (
|
{agents.map((agent) => (
|
||||||
|
|||||||
@@ -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<AgentResetResult>(
|
||||||
|
`/api/cases/${caseNumber}/agents/reset`,
|
||||||
|
{ method: "POST" },
|
||||||
|
),
|
||||||
|
onSuccess: () => {
|
||||||
|
if (caseNumber) {
|
||||||
|
qc.invalidateQueries({ queryKey: agentKeys.activity(caseNumber) });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ from web.paperclip_client import (
|
|||||||
post_comment as pc_post_comment,
|
post_comment as pc_post_comment,
|
||||||
reject_interaction as pc_reject_interaction,
|
reject_interaction as pc_reject_interaction,
|
||||||
reset_agent_session as pc_reset_agent_session,
|
reset_agent_session as pc_reset_agent_session,
|
||||||
|
reset_case_agents as pc_reset_case_agents,
|
||||||
respond_to_interaction as pc_respond_to_interaction,
|
respond_to_interaction as pc_respond_to_interaction,
|
||||||
restore_project as pc_restore_project,
|
restore_project as pc_restore_project,
|
||||||
update_project_name as pc_update_project_name,
|
update_project_name as pc_update_project_name,
|
||||||
@@ -114,4 +115,5 @@ __all__ = [
|
|||||||
"pc_get_run_events",
|
"pc_get_run_events",
|
||||||
"pc_cancel_run",
|
"pc_cancel_run",
|
||||||
"pc_reset_agent_session",
|
"pc_reset_agent_session",
|
||||||
|
"pc_reset_case_agents",
|
||||||
]
|
]
|
||||||
|
|||||||
14
web/app.py
14
web/app.py
@@ -72,6 +72,7 @@ from web.agent_platform_port import (
|
|||||||
pc_reject_interaction,
|
pc_reject_interaction,
|
||||||
pc_request,
|
pc_request,
|
||||||
pc_reset_agent_session,
|
pc_reset_agent_session,
|
||||||
|
pc_reset_case_agents,
|
||||||
pc_respond_to_interaction,
|
pc_respond_to_interaction,
|
||||||
pc_restore_project,
|
pc_restore_project,
|
||||||
pc_wake_analyst_for_appraiser_facts,
|
pc_wake_analyst_for_appraiser_facts,
|
||||||
@@ -4173,6 +4174,19 @@ async def api_post_interaction_response(
|
|||||||
raise HTTPException(502, f"שגיאת Paperclip: {e}")
|
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 ────────────────────────────
|
# ── Settings: MCP Server Configuration ────────────────────────────
|
||||||
#
|
#
|
||||||
# Source of truth for legal-ai env vars is Coolify (see memory:
|
# Source of truth for legal-ai env vars is Coolify (see memory:
|
||||||
|
|||||||
@@ -791,6 +791,71 @@ async def reset_agent_session(agent_id: str) -> dict:
|
|||||||
return resp.json()
|
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(
|
async def respond_to_interaction(
|
||||||
issue_id: str, interaction_id: str, payload: dict,
|
issue_id: str, interaction_id: str, payload: dict,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
|
|||||||
Reference in New Issue
Block a user