Files
legal-ai/web-ui/src/lib/api/agents.ts
Chaim e5168fe79d refactor(web-ui): disambiguate Paperclip agent DTO; document platform-presentation boundary (R3, G12, #112)
ממצא: התוכנית המקורית (namespace ל-paperclip.* ב-types.ts) בלתי-ישימה — types.ts
נוצר-אוטומטית מ-OpenAPI ("Do not make direct changes"); הפניות-Paperclip שם רק משקפות
את ה-API של ה-backend, ונשלטות ע"י מודלי-ה-Pydantic, לא ע"י הפרונט. הפרונט אינו
שכבת-אינטליגנציה — הפניות-Paperclip בו הן הצגת-נתוני-פלטפורמה (activity feed, קישור
לדאשבורד, סטטוס-ארכוב) או UI-ניהול מוצהר (paperclip-tab/agents-tab) — כולן shell-adjacent
לגיטימי תחת G12.

הבעיה האמיתית-והישימה: התנגשות-שם — `PaperclipAgent` הוגדר פעמיים עם shapes שונים
(config ב-paperclip-agents.ts מול activity ב-agents.ts). פוצל: ה-activity-DTO →
`PaperclipAgentStatus`; ה-config שומר `PaperclipAgent`. + הערת-כותרת שמסמנת את agents.ts
כמודול הצגת-פלטפורמה מוצהר.

מזין את R4 (#113): leak-guard חייב להחריג קבצים-נוצרים (types.ts) ולא לכלול את הפרונט
בהיקף שכבת-האינטליגנציה המוגנת.

אימות: tsc --noEmit נקי; eslint נקי על הקבצים ששונו.

Invariants: G12 (גבול-פלטפורמה מוצהר בפרונט), G2 (הסרת שם-טיפוס כפול-משמעות).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 09:33:44 +00:00

182 lines
5.0 KiB
TypeScript

/**
* Paperclip agent ACTIVITY hooks — mirror live agent work into the Legal-AI UI.
*
* Frontend platform-presentation module (G12 / docs/spec/X15): the web-ui is a
* presentation layer, and showing the agent platform's live activity is a
* legitimate, declared use of platform data. Distinct from
* `paperclip-agents.ts`, which is the settings/management view of an agent's
* full config — hence the activity DTO here is `PaperclipAgentStatus`, not the
* config `PaperclipAgent` (the two are different projections; the name no
* longer collides).
*/
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "./client";
// ── Types ────────────────────────────────────────────────────────
export type PaperclipIssue = {
id: string;
title: string;
status: string;
identifier: string;
priority: string;
assignee_name: string | null;
started_at: string | null;
completed_at: string | null;
created_at: string | null;
company_id: string;
};
export type PaperclipComment = {
id: string;
issue_id: string;
body: string;
created_at: string | null;
author_agent_id: string | null;
author_user_id: string | null;
agent_name: string | null;
agent_role: string | null;
agent_icon: string | null;
};
export type PaperclipAgentStatus = {
id: string;
name: string;
role: string;
title: string | null;
status: string;
icon: string | null;
last_heartbeat_at: string | null;
};
export type InteractionKind =
| "ask_user_questions"
| "request_confirmation"
| "suggest_tasks";
export type InteractionStatus =
| "pending"
| "answered"
| "accepted"
| "rejected"
| "expired"
| "failed";
export type InteractionOption = {
id: string;
label: string;
description?: string | null;
};
export type InteractionQuestion = {
id: string;
prompt: string;
selectionMode?: "single" | "multi";
required?: boolean;
options: InteractionOption[];
};
export type InteractionTask = {
clientKey: string;
parentClientKey?: string | null;
title: string;
description?: string | null;
};
/** Free-form payload — shape depends on `kind`. Common fields surfaced for the
* UI; everything else is preserved on the wire. */
export type InteractionPayload = {
version?: number;
submitLabel?: string;
acceptLabel?: string;
rejectLabel?: string;
questions?: InteractionQuestion[];
tasks?: InteractionTask[];
body?: string;
[key: string]: unknown;
};
export type Interaction = {
id: string;
issue_id: string;
kind: InteractionKind;
status: InteractionStatus;
title: string | null;
summary: string | null;
payload: InteractionPayload;
result: Record<string, unknown> | null;
created_at: string | null;
resolved_at: string | null;
};
export type AgentActivityResponse = {
issues: PaperclipIssue[];
comments: PaperclipComment[];
agents: PaperclipAgentStatus[];
interactions: Interaction[];
};
export type InteractionAction = "respond" | "accept" | "reject";
export type InteractionSubmitVars = {
issue_id: string;
interaction_id: string;
action: InteractionAction;
payload: Record<string, unknown>;
};
// ── Query Keys ───────────────────────────────────────────────────
export const agentKeys = {
activity: (caseNumber: string) =>
["agents", "activity", caseNumber] as const,
};
// ── Hooks ────────────────────────────────────────────────────────
export function useAgentActivity(caseNumber: string | undefined) {
return useQuery({
queryKey: agentKeys.activity(caseNumber ?? ""),
queryFn: ({ signal }) =>
apiRequest<AgentActivityResponse>(
`/api/cases/${caseNumber}/agents`,
{ signal },
),
enabled: !!caseNumber,
refetchInterval: 10_000,
});
}
export function useSendComment(caseNumber: string | undefined) {
const qc = useQueryClient();
return useMutation({
mutationFn: (vars: { body: string; issue_id?: string }) =>
apiRequest<{ comment_id: string; issue_id: string; issue_identifier: string }>(
`/api/cases/${caseNumber}/agents/comment`,
{ method: "POST", body: vars },
),
onSuccess: () => {
if (caseNumber) {
qc.invalidateQueries({ queryKey: agentKeys.activity(caseNumber) });
}
},
});
}
export function useSubmitInteraction(caseNumber: string | undefined) {
const qc = useQueryClient();
return useMutation({
mutationFn: (vars: InteractionSubmitVars) =>
apiRequest<Interaction>(
`/api/cases/${caseNumber}/agents/interaction-response`,
{ method: "POST", body: vars },
),
onSuccess: () => {
if (caseNumber) {
qc.invalidateQueries({ queryKey: agentKeys.activity(caseNumber) });
}
},
});
}