PluginProbe
Extendify / 3.2.1
Extendify v3.2.1
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.2.1, at src/Agent/api.js

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