PluginProbe
Extendify / 2.2.0
Extendify v2.2.0
3.2.0 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.6 3.0.5 3.0.4 trunk 0.1.0 0.10.0 0.10.1 0.10.2 0.11.0 0.11.1 0.2.0 0.3.0 0.3.1 0.4.0 0.5.0 0.6.0 0.7.0 All 126 releases
extendify / src / Agent / Agent.jsx

Agent.jsx in Extendify 2.2.0, at src/Agent/Agent.jsx

500 lines 14.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import {
2 useMemo,
3 useEffect,
4 useState,
5 useRef,
6 useCallback,
7 } from '@wordpress/element';
8 import { __ } from '@wordpress/i18n';
9 import { Chat } from '@agent/Chat';
10 import { pickWorkflow, handleWorkflow, callTool, digest } from '@agent/api';
11 import { ChatInput } from '@agent/components/ChatInput';
12 import { ChatMessages } from '@agent/components/ChatMessages';
13 import { ChatSuggestions } from '@agent/components/ChatSuggestions';
14 import { PageDocument } from '@agent/components/PageDocument';
15 import { WelcomeScreen } from '@agent/components/WelcomeScreen';
16 import { UsageMessage } from '@agent/components/messages/UsageMessage';
17 import { useChatStore } from '@agent/state/chat';
18 import { useGlobalStore } from '@agent/state/global';
19 import { useWorkflowStore } from '@agent/state/workflows';
20
21 const devmode = window.extSharedData.devbuild;
22 // Used to abort when wf canceled - reset in cleanup()
23 let controller = new AbortController();
24
25 export const Agent = () => {
26 const { hasMessages, addMessage } = useChatStore();
27 const {
28 mergeWorkflowData,
29 getWorkflow,
30 workflowData,
31 setWorkflow,
32 addWorkflowResult,
33 setWhenFinishedToolProps,
34 whenFinishedToolProps,
35 getAvailableWorkflows,
36 block,
37 setBlock,
38 } = useWorkflowStore();
39 const workflowIds = getAvailableWorkflows().map((w) => w.id);
40 const {
41 open,
42 setOpen,
43 showSuggestions,
44 setShowSuggestions,
45 updateRetryAfter,
46 isChatAvailable,
47 } = useGlobalStore();
48 const [canType, setCanType] = useState(true);
49 const agentWorking = useRef(false);
50 const toolWorking = useRef(false);
51 const [waitingOnToolOrUser, setWaitingOnToolOrUser] = useState(false);
52 const [loop, setLoop] = useState(0);
53 const workflow = getWorkflow();
54 const chatAvailable = useMemo(() => isChatAvailable(), [isChatAvailable]);
55
56 const cleanup = useCallback(() => {
57 setCanType(true);
58 agentWorking.current = false;
59 setWaitingOnToolOrUser(false);
60 controller = new AbortController();
61 block && setBlock(null);
62 window.dispatchEvent(new Event('extendify-agent:remove-block-highlight'));
63 const c = Array.from(
64 document.querySelectorAll(
65 '#extendify-agent-chat-scroll-area div:last-child',
66 ),
67 )?.at(-1);
68 c?.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
69 c?.scrollBy({ top: -5, behavior: 'smooth' });
70 }, [setBlock, block]);
71
72 const findAgent = useCallback(
73 async (options = {}) => {
74 addMessage('status', { type: 'calling-agent' });
75 const response = await pickWorkflow({
76 workflows: workflowIds,
77 options: { signal: controller.signal, ...options },
78 }).catch((error) => {
79 devmode && console.error(error);
80 if (error?.response?.status === 429) {
81 updateRetryAfter(error?.response?.headers?.get('Retry-After'));
82 setCanType(false);
83 addMessage('status', { type: 'credits-exhausted' });
84 return;
85 }
86 setCanType(true);
87 if (error === 'Workflow aborted') {
88 addMessage('status', { type: 'workflow-canceled' });
89 return;
90 }
91 addMessage('status', { type: 'error' });
92 return;
93 });
94 if (!response) return;
95
96 const { workflow: wf, reply } = response;
97 if (wf?.id) setWorkflow(wf);
98 if (reply) {
99 const data = { role: 'assistant', content: reply, agent: wf?.agent };
100 addMessage('message', data);
101 }
102 if (!wf?.id) setCanType(true);
103 },
104 [addMessage, updateRetryAfter, setWorkflow, workflowIds],
105 );
106
107 const handleSubmit = useCallback(
108 async (message) => {
109 setShowSuggestions(false);
110 setWaitingOnToolOrUser(false);
111 agentWorking.current = false;
112 addMessage('message', { role: 'user', content: message });
113 setCanType(false);
114 // If they typed while waiting on a redirect, reset the workflow
115 if (workflow?.needsRedirect?.()) {
116 setWorkflow(null);
117 }
118 if (workflow && !workflow?.needsRedirect?.()) {
119 // reset the workflow to let the effect handle it
120 const wfData = workflowData || {};
121 setWorkflow({ ...workflow });
122 mergeWorkflowData(wfData);
123 return;
124 }
125 await findAgent().catch((e) => devmode && console.error(e));
126 },
127 [
128 addMessage,
129 findAgent,
130 mergeWorkflowData,
131 setWorkflow,
132 workflow,
133 workflowData,
134 setShowSuggestions,
135 ],
136 );
137
138 useEffect(() => {
139 // Allow external messages to trigger the agent
140 const handleMessage = ({ detail }) => {
141 if (!detail?.message) return;
142 handleSubmit(detail.message);
143 };
144 // Allow external code to clear the block and workflow
145 const handleCleanup = () => {
146 controller.abort('Workflow aborted');
147 setWorkflow(null);
148 cleanup();
149 addMessage('status', { type: 'workflow-canceled' });
150 return;
151 };
152 window.addEventListener('extendify-agent:cancel-workflow', handleCleanup);
153 window.addEventListener('extendify-agent:chat-submit', handleMessage);
154 return () => {
155 window.removeEventListener(
156 'extendify-agent:cancel-workflow',
157 handleCleanup,
158 );
159 window.removeEventListener('extendify-agent:chat-submit', handleMessage);
160 };
161 }, [handleSubmit, cleanup, setWorkflow, addMessage]);
162
163 // Handle whenFinished component confirm/cancel
164 useEffect(() => {
165 const handleConfirm = async ({ detail }) => {
166 if (toolWorking.current) return;
167 toolWorking.current = true;
168 const { data, whenFinishedToolProps } = detail ?? {};
169 const { summary, status, whenFinishedTool, answerId } =
170 whenFinishedToolProps.agentResponse;
171 const { id, labels } = whenFinishedTool || {};
172 // Not all workflows have a tool at the end (e.g. tours)
173 const toolResponse = await callTool?.({ tool: id, inputs: data }).catch(
174 (error) => {
175 const { sessionId } = workflow || {};
176 digest({ caller: `when-finished: ${id}`, sessionId, error });
177 devmode && console.error(error);
178 return { error: error.message };
179 },
180 );
181 toolWorking.current = false;
182 // Add the workflow result to the history
183 addWorkflowResult({
184 answerId,
185 agentName: workflow?.agent?.name,
186 summary,
187 status,
188 errorMsg: toolResponse?.error,
189 });
190 if (toolResponse?.error) {
191 addMessage('status', { type: 'error' });
192 setWorkflow(null);
193 cleanup();
194 return;
195 }
196 addMessage('status', {
197 label: labels?.confirm,
198 type: 'workflow-tool-completed',
199 });
200 addMessage('workflow', {
201 status: 'completed',
202 agent: workflow.agent,
203 answerId,
204 });
205 setWorkflow(null);
206 cleanup();
207 };
208 const handleCancel = ({ detail }) => {
209 if (toolWorking.current) return;
210 const { summary, whenFinishedTool, answerId } =
211 detail.whenFinishedToolProps.agentResponse;
212 addMessage('workflow', {
213 status: 'canceled',
214 agent: workflow.agent,
215 answerId,
216 });
217 addWorkflowResult({
218 answerId,
219 summary,
220 status: 'canceled',
221 agentName: workflow?.agent?.name,
222 });
223 setWorkflow(null);
224 cleanup();
225 addMessage('status', {
226 label: whenFinishedTool?.labels?.cancel,
227 type: 'workflow-tool-canceled',
228 });
229 };
230 window.addEventListener('extendify-agent:workflow-confirm', handleConfirm);
231 window.addEventListener('extendify-agent:workflow-cancel', handleCancel);
232 return () => {
233 window.removeEventListener(
234 'extendify-agent:workflow-confirm',
235 handleConfirm,
236 );
237 window.removeEventListener(
238 'extendify-agent:workflow-cancel',
239 handleCancel,
240 );
241 };
242 }, [addMessage, cleanup, addWorkflowResult, setWorkflow, workflow]);
243
244 useEffect(() => {
245 const handleClose = () => setOpen(false);
246 const handleOpen = () => setOpen(true);
247 window.addEventListener('extendify-agent:close', handleClose);
248 window.addEventListener('extendify-agent:open', handleOpen);
249 return () => {
250 window.removeEventListener('extendify-agent:close', handleClose);
251 window.removeEventListener('extendify-agent:open', handleOpen);
252 };
253 }, [setOpen]);
254
255 useEffect(() => {
256 if (waitingOnToolOrUser || !open || !workflow?.id) return;
257 // Some workflows require they dont change pages
258 const theyMoved = workflow?.startingPage !== window.location.href;
259 // Requires a block to be selected
260 const blockMissing = !block && workflow?.requires?.includes('block');
261 const cancelWorkflow =
262 (workflow?.cancelOnPageChange && theyMoved) || blockMissing;
263 if (cancelWorkflow) {
264 addMessage('workflow', { status: 'canceled', agent: workflow.agent });
265 setWorkflow(null);
266 cleanup();
267 return;
268 }
269 // A component is running
270 if (whenFinishedToolProps?.id) return;
271 // They must be on a page where they can do work
272 if (workflow?.needsRedirect?.()) {
273 cleanup();
274 return;
275 }
276 (async () => {
277 if (agentWorking.current) return; // Prevent multiple calls
278 setCanType(false);
279 setShowSuggestions(false);
280 agentWorking.current = true;
281 addMessage('status', { type: 'agent-working' });
282 const agentResponse = await handleWorkflow({
283 workflow,
284 workflowData,
285 options: { signal: controller.signal },
286 }).catch((error) => {
287 if (error === 'Workflow aborted') {
288 addMessage('status', { type: 'workflow-canceled' });
289 setWorkflow(null);
290 cleanup();
291 return;
292 }
293 const { sessionId } = workflow || {};
294 digest({ caller: 'handle-workflow', sessionId, error });
295 devmode && console.error(error);
296 return { error: error.message };
297 });
298 if (!agentResponse) return;
299 const { summary, status, answerId } = agentResponse;
300 // Add the workflow result to the history
301 addWorkflowResult({
302 answerId,
303 summary,
304 status,
305 errorMsg: agentResponse?.error,
306 agentName: workflow?.agent?.name,
307 });
308 if (!open) return;
309 if (agentResponse.error) {
310 // mutate the window to add failed tools rather than keep state
311 window.extAgentData.failedWorkflows =
312 window.extAgentData.failedWorkflows || new Set();
313 window.extAgentData.failedWorkflows.add(workflow.id);
314 throw new Error(`Error handling workflow: ${agentResponse.error}`);
315 }
316
317 // The ai sent back some text to show to the user
318 if (agentResponse.reply) {
319 addMessage('message', {
320 role: 'assistant',
321 content: agentResponse.reply,
322 followup: !!agentResponse.tool,
323 pageSuggestion: agentResponse.pageSuggestion,
324 agent: workflow.agent,
325 sessionId: workflow?.sessionId,
326 });
327 }
328 // This is at the end of the workflow
329 // and we are about to execute the final tool
330 if (agentResponse.whenFinishedTool?.id) {
331 setWhenFinishedToolProps({
332 ...agentResponse.whenFinishedTool,
333 agentResponse,
334 });
335 // If static, add it as a message
336 const { id, inputs, static: staticC } = agentResponse.whenFinishedTool;
337 if (staticC) {
338 addMessage('workflow-component', { id, status: 'completed', inputs });
339 setWorkflow(null);
340 addMessage('workflow', {
341 status: 'completed',
342 agent: workflow.agent,
343 answerId,
344 });
345 cleanup();
346 }
347 return;
348 }
349 // Agent thinks it needs to handoff to another agent
350 if (agentResponse.status === 'handoff') {
351 const currentWorkflowId = workflow.id;
352 setWorkflow(null);
353 cleanup();
354 await findAgent({ handoff: currentWorkflowId });
355 return;
356 }
357 // If we're done, it means the AI has the answer
358 if (agentResponse.status === 'completed') {
359 setWorkflow(null);
360 cleanup();
361 addMessage('workflow', {
362 status: 'completed',
363 agent: workflow.agent,
364 answerId,
365 });
366 return;
367 }
368 if (agentResponse.status === 'canceled') {
369 setWorkflow(null);
370 cleanup();
371 addMessage('workflow', {
372 status: 'canceled',
373 agent: workflow.agent,
374 answerId,
375 });
376 return;
377 }
378 // These inputs are filled out by the AI
379 mergeWorkflowData(agentResponse.inputs);
380 // Agent needs more info from a
381 if (agentResponse.tool) {
382 const { id, inputs, labels } = agentResponse.tool;
383 addMessage('status', { label: labels?.started, type: 'tool-started' });
384 const toolData = await Promise.all([
385 callTool({ tool: id, inputs }),
386 new Promise((resolve) => setTimeout(resolve, 3000)),
387 ])
388 .then(([data]) => data)
389 .catch((error) => {
390 const { sessionId } = workflow || {};
391 digest({ caller: `in-progress: ${id}`, sessionId, error });
392 devmode && console.error(error);
393 throw error;
394 });
395 addMessage('status', {
396 label: labels?.confirm,
397 type: 'tool-completed',
398 });
399 await new Promise((resolve) => setTimeout(resolve, 1000));
400 mergeWorkflowData(toolData);
401 setWaitingOnToolOrUser(false);
402 agentWorking.current = false;
403 setLoop((prev) => prev + 1); // Trigger next loop
404 return;
405 }
406 setCanType(true);
407 setWaitingOnToolOrUser(true);
408 })().catch((error) => {
409 const { sessionId } = workflow || {};
410 digest({ caller: 'main-loop', sessionId, error });
411 devmode && console.error(error);
412 setWorkflow(null);
413 cleanup();
414 addMessage('status', { type: 'error' });
415 });
416 }, [
417 loop,
418 cleanup,
419 addWorkflowResult,
420 open,
421 workflow,
422 workflowData,
423 addMessage,
424 setWorkflow,
425 agentWorking,
426 waitingOnToolOrUser,
427 mergeWorkflowData,
428 canType,
429 findAgent,
430 setShowSuggestions,
431 whenFinishedToolProps,
432 setWhenFinishedToolProps,
433 block,
434 ]);
435
436 useEffect(() => {
437 if (!canType) return;
438 document.querySelector('#extendify-agent-chat-textarea')?.focus();
439 }, [canType]);
440
441 const showWelcomeScreen = !hasMessages();
442 const showPromptSuggestions =
443 !workflow?.id &&
444 !showWelcomeScreen &&
445 chatAvailable &&
446 showSuggestions &&
447 !block;
448 const busy = !canType || !chatAvailable || workflow?.id;
449
450 return (
451 <Chat busy={busy}>
452 <div className="relative z-50 flex h-full flex-col justify-between overflow-auto border-t border-solid border-gray-300">
453 {showWelcomeScreen ? (
454 <div
455 className="relative flex flex-grow flex-col overflow-y-auto overflow-x-hidden"
456 style={{
457 backgroundImage:
458 'linear-gradient( to bottom, #f0f0f0 0%, #fff 60%, #fff 100%)',
459 }}>
460 <div className="flex-grow" />
461 <WelcomeScreen />
462 </div>
463 ) : (
464 <ChatMessages
465 redirectComponent={
466 workflow?.needsRedirect?.() ? workflow.redirectComponent : null
467 }
468 />
469 )}
470
471 <div>
472 <div className="relative flex flex-col px-4 pb-2 pt-2.5 shadow-lg-flipped">
473 {showPromptSuggestions ? <ChatSuggestions /> : null}
474 {block ? <PageDocument busy={busy} blockId={block.id} /> : null}
475 <UsageMessage
476 onReady={() => {
477 cleanup();
478 setShowSuggestions(true);
479 addMessage('status', { type: 'credits-restored' });
480 }}
481 />
482 </div>
483 <div className="p-4 pb-2 pt-0">
484 <ChatInput
485 disabled={!canType || !chatAvailable}
486 handleSubmit={handleSubmit}
487 />
488 </div>
489 <div className="text-pretty px-4 pb-2 text-center text-xss leading-none text-gray-600">
490 {__(
491 'AI Agent can make mistakes. Check changes before saving.',
492 'extendify-local',
493 )}
494 </div>
495 </div>
496 </div>
497 </Chat>
498 );
499 };
500