PluginProbe
Extendify / 3.2.2
Extendify v3.2.2
3.2.2 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 All 128 releases
extendify / src / Agent / api.js

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

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