PluginProbe
Extendify / 3.0.4
Extendify v3.0.4
3.2.1 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 All 127 releases
extendify / src / Agent / Agent.jsx

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

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