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