PluginProbe
Extendify / 3.1.6
Extendify v3.1.6
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 / api.js

api.js in Extendify 3.1.6, at src/Agent/api.js

275 lines 9.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { abilityDescriptors, getAbilities } from '@agent/abilities/abilities';
2 import { buildBlockSchema } from '@agent/lib/block-schema';
3 import {
4 classifyBlockEdit,
5 IGNORED_BLOCKS,
6 } from '@agent/lib/classify-block-edit';
7 import { getClientTools } from '@agent/lib/client-tools';
8 import { INSERTABLE_BLOCK_TYPES } from '@agent/lib/insertable-blocks';
9 import { ensureCoreBlocksRegistered } from '@agent/lib/register-blocks';
10 import {
11 buildSubtreeManifest,
12 buildSubtreeTree,
13 } from '@agent/lib/subtree-manifest';
14 import { activeCanvasStep } from '@agent/state/canvas';
15 import { useChatStore } from '@agent/state/chat';
16 import { useGlobalStore } from '@agent/state/global';
17 import { tools } from '@agent/workflows/workflows';
18 import { AI_HOST } from '@constants';
19 import { useQuickEditStore } from '@quick-edit/state/store';
20 import { digest } from '@shared/api/digest';
21 import { reqDataBasics } from '@shared/lib/data';
22 import { getBlockType } from '@wordpress/blocks';
23
24 const rootFor = (block) =>
25 block?.id && block?.target
26 ? document.querySelector(`[${block.target}="${block.id}"]`)
27 : null;
28
29 // If greater than 5 blocks the agent will narrow the scope
30 const EAGER_LOAD_MAX = 5;
31
32 // The staged selection's attribute schemas, with the install's palette baked in.
33 export const blockSchemasFor = async (block) => {
34 const root = rootFor(block);
35 const { bucket } = classifyBlockEdit({ block, root });
36 const types =
37 bucket === 'single'
38 ? [block.blockType]
39 : IGNORED_BLOCKS.has(block?.blockType)
40 ? []
41 : [...new Set(buildSubtreeManifest(root).map(({ type }) => type))];
42 if (!types.length || types.length > EAGER_LOAD_MAX) return [];
43 await ensureCoreBlocksRegistered();
44 return types
45 .map((type) => ({ type, schema: buildBlockSchema(getBlockType(type)) }))
46 .filter(({ schema }) => schema);
47 };
48
49 // Only consumed for a multi-block edit; inert for single/combo.
50 export const subtreeManifestFor = (block) =>
51 buildSubtreeManifest(rootFor(block));
52
53 // Selection hierarchy for the backend's positional edits.
54 export const subtreeTreeFor = (block) => buildSubtreeTree(rootFor(block));
55
56 const extra = () => {
57 const { x, y, width, height } = useGlobalStore.getState();
58 return {
59 userAgent: window?.navigator?.userAgent,
60 vendor: window?.navigator?.vendor || 'unknown',
61 platform:
62 window?.navigator?.userAgentData?.platform ||
63 window?.navigator?.platform ||
64 'unknown',
65 mobile: window?.navigator?.userAgentData?.mobile,
66 width: window.innerWidth,
67 height: window.innerHeight,
68 screenHeight: window.screen.height,
69 screenWidth: window.screen.width,
70 orientation: window.screen.orientation?.type,
71 touchSupport: 'ontouchstart' in window || navigator.maxTouchPoints > 0,
72 agentUI: { x, y, width, height },
73 };
74 };
75
76 export const pickWorkflow = async ({ workflows, options }) => {
77 const { failedWorkflows, context } = window.extAgentData;
78 const failed = failedWorkflows ?? new Set();
79 const filteredWorkflows = workflows.filter((wf) => !failed.has(wf.id));
80
81 const block = useQuickEditStore.getState().agentBlock;
82
83 const messages = useChatStore
84 .getState()
85 .getCurrentMessages({ includeTools: false });
86 const lastAssistantMessage = useChatStore
87 .getState()
88 .getLastAssistantMessage();
89
90 const response = await fetch(`${AI_HOST}/api/agent/find-agent`, {
91 method: 'POST',
92 headers: { 'Content-Type': 'application/json' },
93 signal: options?.signal,
94 body: JSON.stringify({
95 ...reqDataBasics,
96 workflows: filteredWorkflows,
97 previousWorkflow: {
98 workflowId: lastAssistantMessage?.details?.workflowId,
99 language: lastAssistantMessage?.details?.language,
100 lastMessage: lastAssistantMessage?.details?.content,
101 sessionId: lastAssistantMessage?.details?.sessionId,
102 },
103 context,
104 agentContext: window.extAgentData.agentContext,
105 wpAbilities: window.extAgentData.wpAbilities ?? [],
106 clientTools: getClientTools(),
107 messages: messages.slice(-5),
108 hasBlock: Boolean(block), // todo: remove this
109 blockDetails: block,
110 ...options,
111 extra: extra(),
112 }),
113 });
114
115 if (!response.ok) {
116 digest({
117 error: {
118 name: response.statusText,
119 messages: response.statusMessage,
120 },
121 details: { source: 'agent', caller: 'pick-workflow' },
122 });
123 const error = new Error('Bad response from server');
124 error.response = response;
125 throw error;
126 }
127 return await response.json();
128 };
129
130 export const handleWorkflow = async ({ workflow, workflowData, options }) => {
131 const { getCurrentMessages, getMessagesFor } = useChatStore.getState();
132 const block = useQuickEditStore.getState().agentBlock;
133 const isBlockPatching =
134 workflow?.id === 'block-patching' ||
135 (workflow?.id === 'select-block' && Boolean(block));
136 // Spent once the manifest flows: patching never reads this workflow's inputs.
137 const { blockSearch: _spent, ...carried } = workflowData ?? {};
138 // Schemas: pinned up front when the selection is small enough; otherwise
139 // empty until get-block-schemas fetches them into the same field.
140 const data = isBlockPatching
141 ? {
142 ...carried,
143 blockSchemas: workflowData?.blockSchemas?.length
144 ? workflowData.blockSchemas
145 : await blockSchemasFor(block),
146 }
147 : workflowData;
148 const response = await fetch(`${AI_HOST}/api/agent/handle-workflow`, {
149 method: 'POST',
150 headers: { 'Content-Type': 'application/json' },
151 signal: options?.signal,
152 body: JSON.stringify({
153 ...reqDataBasics,
154 workflow,
155 workflowData: data,
156 messages: getCurrentMessages(),
157 previousMessages: getMessagesFor(workflow?.id),
158 context: window.extAgentData.context,
159 agentContext: window.extAgentData.agentContext,
160 wpAbilities: window.extAgentData.wpAbilities ?? [],
161 clientTools: getClientTools(),
162 // The manifest is the backend's block-patching signal — never send it
163 // for other workflows or they'd be routed into the patching handler.
164 blockManifest: isBlockPatching ? subtreeManifestFor(block) : [],
165 blockTree: isBlockPatching ? subtreeTreeFor(block) : [],
166 // The plugin owns the add catalog so old fielded builds are never
167 // offered a type they can't build.
168 insertableBlockTypes: isBlockPatching ? INSERTABLE_BLOCK_TYPES : [],
169 retry: options?.retry || false,
170 features: ['qa-tool'],
171 extra: extra(),
172 }),
173 });
174
175 if (!response.ok) throw new Error('Bad response from server');
176 return await response.json();
177 };
178
179 export const handleCanvas = async ({
180 toolId,
181 sessionId,
182 abilities,
183 options,
184 }) => {
185 const { getCurrentMessages, getMessagesFor } = useChatStore.getState();
186 const { values } = activeCanvasStep();
187 const response = await fetch(`${AI_HOST}/api/agent/handle-canvas`, {
188 method: 'POST',
189 headers: { 'Content-Type': 'application/json' },
190 signal: options?.signal,
191 body: JSON.stringify({
192 ...reqDataBasics,
193 toolId,
194 values,
195 abilities: abilityDescriptors(getAbilities(abilities)),
196 messages: getCurrentMessages(),
197 previousMessages: getMessagesFor(toolId),
198 context: window.extAgentData.context,
199 sessionId,
200 extra: extra(),
201 }),
202 });
203
204 if (!response.ok) throw new Error('Bad response from server');
205 return await response.json();
206 };
207
208 export const rateAnswer = ({ answerId, rating }) =>
209 fetch(`${AI_HOST}/api/agent/rate-workflow`, {
210 method: 'POST',
211 headers: { 'Content-Type': 'application/json' },
212 body: JSON.stringify({ answerId, rating }),
213 }).catch((error) =>
214 digest({
215 error: error,
216 details: { source: 'agent', caller: 'rateAnswer', answerId, rating },
217 }),
218 );
219
220 // An error with no message serializes to {}, which the model reads as an empty success.
221 const reasonFor = async (error) => {
222 // An unparsed apiFetch rejects with the Response, whose body holds the reason.
223 if (typeof error?.json === 'function') {
224 const body = await error.json().catch(() => null);
225 return body?.message ?? `The site answered ${error.status}.`;
226 }
227
228 return (
229 error?.error ?? error?.message ?? 'The ability failed without saying why.'
230 );
231 };
232
233 // A throw would reach the model outside the tool's slot, reading as no result.
234 const runAbility = async (ability, inputs) => {
235 try {
236 return await ability.execute(inputs);
237 } catch (error) {
238 return { error: await reasonFor(error) };
239 }
240 };
241
242 export const callTool = async ({ tool, inputs, abilities = [] }) => {
243 if (tools[tool]) return await tools[tool](inputs);
244 // Ours wins a name collision, so a duplicate WP ability can't change what runs.
245 const ours =
246 getAbilities(abilities).find(({ name }) => name === tool) ??
247 getAbilities([tool])[0];
248 if (ours) return { [tool]: await runAbility(ours, inputs) };
249 // Ability tools are named after the ability and have no file; the generic
250 // runner executes them. Key the result to its slot so the loop sees it filled.
251 const isAbility = (window.extAgentData?.wpAbilities ?? []).some((category) =>
252 category.abilities?.some((ability) => ability.name === tool),
253 );
254 if (isAbility) {
255 return {
256 [tool]: await tools['execute-ability']({ ability: tool, input: inputs }),
257 };
258 }
259 throw new Error(`Tool ${tool} not found`);
260 };
261
262 export const recordAgentActivity = ({ action, sessionId, value = {} }) => {
263 return fetch(`${AI_HOST}/api/agent/activities`, {
264 keepalive: true,
265 method: 'POST',
266 headers: { 'Content-Type': 'application/json' },
267 body: JSON.stringify({
268 ...reqDataBasics,
269 action,
270 sessionId,
271 value,
272 }),
273 });
274 };
275