Woo Agent
AI agent designed to automate WooCommerce tasks.
Install
Instructions and Source Code
You are an AI agent designed to assist with WordPress and WooCommerce tasks. Your name is Woo. General Guidelines: - Don't make assumptions - Don't guess - Do only what was asked of you. - To continue the chat without user interaction generate /metaAction(execute_and_callback) - Avoid large responses, instead generate few commands + /metaAction(execute_and_callback) and wait for response to continue Flow 1: WordPress Event Processing: - You could receive WordPress events in JSON format with injected prompts (e.g., {"desc": "iPhone 16 Pro {woo generate more}"}). - Automatically process the event to completion. - Avoid user interactions, as the user does not monitor the chat. - After completing the task, generate /jobCompleted({"post_id": 123, "message": "short message"}) (e.g., for editing product 123). Flow 2: Direct User Instructions: - You could receive instructions in a free chat session. - Possible user interaction if needed Flow 3: Develop Plugins: - break large plugins implementation to multiple stages - After each stage wait the user to confirm next features before continue - use the /suggestion("some feature") command to suggest features to be implemented in the next stage - use writeFile MULTILINE_COMMAND to write/edit plugins source code - use FUNCTION_COMMANDS for any additional actions - always deactivate the plugin before code modifications - when requested to modify an existing plugin use read-recursive to check the current state - you can mix different command types in the same message <MULTILINE_COMMANDS> writeFile my-plugin/my-plugin.php ```php <?php echo "Hello, World" ?> ``` writeFile my-plugin/style.css ```css body { margin: 0; } ``` Description: """ writeFile command creates or updates files by given {path} followed by code block Missing folders are created automatically. Always provide complete source code, as it is deployed automatically (as it is) """ </MULTILINE_COMMANDS> <FUNCTION_COMMANDS> Generate Node.js code to perform WordPress related tasks using the provided template functions: Function Guidelines: - Use axios to call wp-json APIs like wp/v2, wc/v3 or openkbs/v1 to assist the user - Generate only openkbs/v1 endpoints listed below - The secrets placeholders are replaced automatically by the system. - Once you generate a function, it will be automatically executed (as it is) and you will get the response. Avoid huge responses and long chats: - Create targeted scripts for specific data lookups instead of fetching entire datasets - Try to complete the Task in less than 10 iterations Template Functions: ```javascript const axios = require('axios'); const handler = async () => { // Generate code here const url = '{{secrets.wpUrl}}'; const headers = { 'WP-API-KEY': '{{secrets.wpapiKey}}' }; try { // WooCommerce API Examples: const response = await axios.get(`${url}/wp-json/wc/v3/products/123`, { headers }); // Get product // Plugins API Examples: const pluginsUrl = `${url}/wp-json/openkbs/v1/plugins` const response = await axios.get(`${pluginsUrl}/list`, headers); // Lists all WordPress plugins with their status const response = await axios.post(`${pluginsUrl}/activate`, {plugin_path: 'my-plugin/my-plugin.php'}, {headers}); const response = await axios.post(`${pluginsUrl}/deactivate`, {plugin_path: 'my-plugin/my-plugin.php'}, {headers}); // Filesystem API Examples: // All filesystem operations are scoped to the WordPress plugins directory // Paths provided should be relative to wp-content/plugins/ const fsUrl = `${url}/wp-json/openkbs/v1/filesystem` const response = await axios.get(`${fsUrl}/list`, {headers}); // Lists all plugins directories const response = await axios.post(`${fsUrl}/write`, { path: 'my-plugin/my-plugin.php', content:`<?php echo 'Hello' ?>` }, {headers}); // deprecated, use multiline writeFile syntax instead const response = await axios.post(`${fsUrl}/mkdir`, { path: 'my-plugin' }, {headers}); // Creates new directory const response = await axios.get(`${fsUrl}/read-recursive`, { params: { path: 'my-plugin' }, headers}) // Gets all content from all files recursively const response = await axios.get(`${fsUrl}/read`, { params: { path: 'my-plugin/my-plugin.php' }, headers}); // Gets contents of a single file const response = await axios.post(`${fsUrl}/copy`, { source: 'my-plugin/my-plugin.php', destination: 'my-plugin/utils.php' }, {headers}); // Copies a file or directory const response = await axios.get(`${fsUrl}/list`, { params: { path: 'my-plugin' }, headers}); // Lists files and directories of a specific plugin directory const response = await axios.delete(`${fsUrl}/delete`, { headers, data: { path: 'my-plugin' } }); // Deletes a specified file or directory return response.data; } catch (error) { return error.response ? error.response.data : error.message; } }; module.exports = { handler }; ``` </FUNCTION_COMMANDS> <SINGLE_LINE_COMMANDS> Use API commands below to assist the user Guidelines: - If an API call fails, so that you can't extract the required data, retry with a different website or search query - Multiple commands can be processed (one per line) /googleSearch("query") Description: """ Get results from Google Search API. """ /webpageToText("URL") Description: """ Use this API to extract text, images, products and other content from website (extracting data from website) """ /viewImage("imageURL") Description: """ Use this API to view the image at the specified URL """ /metaAction(execute_and_callback) Description: """ Executes last commands (if any) and calls back with the responses without waiting for user interaction Always last command """ /metaAction(execute_and_wait) Description: """ Executes last commands (if any) and waits for user interaction Always last command """ /suggestion("free text") Description: """ Renders the suggestion as clickable UI Button, so that the user can easily select next features Always last commands """ /jobCompleted({"post_id": 123, "message": ""}) Description: """ Sets this job as completed """ /jobFailed({"post_id": 123, "message": ""}) Description: """ Sets this job as failed """ </SINGLE_LINE_COMMANDS>
Events Files (Node.js Backend)
Events/actions.js
import vm from 'vm';
import axios from "axios";
// Updated regex to include language and new format
const batchRegex = /(?:writeFile\s+([^\s]+)\s*```(\w+)\s*([\s\S]*?)```|``javascript\s*([\s\S]*?)\s*``|\/?(googleSearch|webpageToText|viewImage|metaAction|suggestion|jobCompleted|jobFailed)\(([^()]*)\))/g;
function detectLazyOutput(text) {
return text.split('\n').some(line => {
const commentContent = line.trim().substring(2).trim().toLowerCase();
return line.trim().startsWith('//') && ['...', 'same'].some(pattern => commentContent.includes(pattern));
});
}
export const getActions = (meta) => [
[batchRegex, async (match, event) => {
// Get the full message content
const lastMessage = event.payload.messages[event.payload.messages.length - 1].content;
let disableAutoCallback = meta?._meta_actions?.includes('REQUEST_CHAT_MODEL_EXCEEDED')
// Find all blocks and commands in order
const blocks = Array.from(lastMessage.matchAll(batchRegex))
.map(([full, filePath, language, fileContent, jsContent, commandType, commandArg]) => {
if (filePath && language && fileContent) {
return {
type: 'writeFile',
path: filePath.trim(),
language: language.trim(),
content: fileContent.trim()
};
} else if (jsContent) {
return {
type: 'javascript',
content: jsContent.trim()
};
} else if (commandType) {
if (commandType === 'suggestion') disableAutoCallback = true; // require human confirmation
if (commandArg?.startsWith('"') && commandArg?.endsWith('"')) {
commandArg = commandArg.slice(1, -1); // remove quotes if any
}
let arg = commandArg.trim();
let isJSON = false;
// Attempt to parse the argument as JSON
try {
arg = JSON.parse(commandArg);
isJSON = true;
} catch (e) {
// Argument is not JSON; proceed with the trimmed string
}
return {
type: commandType,
arg: arg
};
}
});
if (blocks.length === 0) {
return {
error: "No valid blocks or commands found",
...meta
};
}
try {
// Process blocks sequentially in order
const results = [];
let stop;
for (const block of blocks) {
if (stop) break;
if (block.type === 'writeFile' && detectLazyOutput(block.content)) {
results.push({
type: 'writeFile',
path: block.path,
success: false,
error: `Lazy comment detected in writeFile block for path: ${block.path}`
});
continue; // Skip processing this block further to avoid saving broken code
}
const url = '{{secrets.wpUrl}}';
const headers = { 'WP-API-KEY': '{{secrets.wpapiKey}}' };
const handleJobFinished = async (block, url, headers, results) => {
const { arg: data } = block;
const { post_id, message } = data;
const encryptedTitle = await openkbs.encrypt(message);
const promises = [
openkbs.chats({
action: "updateChat",
title: encryptedTitle,
chatIcon: block?.type === 'jobCompleted' ? '🟢' : '🔴',
chatId: event?.payload?.chatId
})
];
if (post_id) {
promises.push(
axios.post(`${url}/wp-json/openkbs/v1/callback`, { post_id, message, type: "reload" }, { headers })
);
}
await Promise.all(promises);
results.push({ type: block.type, success: true, data });
};
switch (block.type) {
case 'writeFile': {
const fsUrl = `${url}/wp-json/openkbs/v1/filesystem`;
const response = await axios.post(
`${fsUrl}/write`,
{ path: block.path, content: block.content },
{ headers }
);
results.push({
type: 'writeFile',
path: block.path,
language: block.language,
success: response.status === 200
});
break;
}
case 'javascript': {
let sourceCode = block.content
.replace(`\{\{secrets.wpapiKey\}\}`, '{{secrets.wpapiKey}}')
.replace(`\{\{secrets.wpUrl\}\}`, '{{secrets.wpUrl}}');
// Add the export statement if it's not already present
if (!sourceCode.includes('module.exports')) sourceCode += '\nmodule.exports = { handler };'
const script = new vm.Script(sourceCode);
const context = {
require: (id) => rootContext.require(id),
...rootContext,
console,
module: { exports: {} }
};
vm.createContext(context);
script.runInContext(context);
const { handler } = context.module.exports;
const data = await handler();
results.push({
type: 'javascript',
success: !data?.error,
data
});
break;
}
case 'googleSearch': {
const noSecrets = '{{secrets.googlesearch_api_key}}'.includes('secrets.googlesearch_api_key');
const params = {
q: block.arg,
...(noSecrets ? {} : {
key: '{{secrets.googlesearch_api_key}}',
cx: '{{secrets.googlesearch_engine_id}}'
})
};
const response = noSecrets
? await openkbs.googleSearch(params.q, params)
: (await axios.get('https://www.googleapis.com/customsearch/v1', { params }))?.data?.items;
const data = response?.map(({ title, link, snippet, pagemap }) => ({
title, link, snippet, image: pagemap?.metatags?.[0]?.["og:image"]
}));
results.push({
type: 'googleSearch',
success: !!data?.length,
data: data || { error: "No results found" }
});
break;
}
case 'webpageToText': {
const response = await openkbs.webpageToText(block.arg);
if (response?.content?.length > 5000) {
response.content = response.content.substring(0, 5000);
}
results.push({
type: 'webpageToText',
success: !!response?.url,
data: response?.url ? response : { error: "Unable to read website" }
});
break;
}
case 'viewImage': {
results.push({
type: 'viewImage',
success: true,
data: [
{ type: "text", text: "Image URL: " + block.arg },
{ type: "image_url", image_url: { url: block.arg } }
]
});
break;
}
case 'metaAction': {
stop = true; // always last command
if (block.arg === 'execute_and_callback') {
if (!disableAutoCallback && !meta._meta_actions.includes('REQUEST_CHAT_MODEL')) {
meta._meta_actions.push('REQUEST_CHAT_MODEL');
}
} else if (block.arg === 'execute_and_wait') {
meta._meta_actions = meta._meta_actions.filter(action => action !== 'REQUEST_CHAT_MODEL');
}
results.push({
type: 'metaAction',
success: true,
data: block.arg
});
break;
}
case 'jobCompleted':
case 'jobFailed':
stop = true;
await handleJobFinished(block, url, headers, results);
break;
}
}
const allSuccessful = results.every(r => r.success);
if (allSuccessful) {
return {
data: {
message: "All operations completed successfully",
results
},
...meta
};
} else {
if (!disableAutoCallback) meta._meta_actions = ["REQUEST_CHAT_MODEL"]
return {
data: {
error: "Some operations failed",
results
},
...meta,
};
}
} catch (e) {
if (!disableAutoCallback) meta._meta_actions = ["REQUEST_CHAT_MODEL"]
return {
error: e.response?.data || e.message,
...meta
};
}
}]
];
Events/onRequest.js
import {getActions} from './actions.js';
export const handler = async (event) => {
const actions = getActions({ _meta_actions: [] });
for (let [regex, action] of actions) {
const lastMessage = event.payload.messages[event.payload.messages.length - 1].content;
const match = lastMessage?.match(regex);
if (match) return await action(match, event);
}
return { type: 'CONTINUE' }
};
Events/onResponse.js
import {getActions} from './actions.js';
export const handler = async (event) => {
const maxSelfInvokeMessagesCount = 30;
const actions = getActions({
_meta_actions: event?.payload?.messages?.length > maxSelfInvokeMessagesCount
? ["REQUEST_CHAT_MODEL_EXCEEDED"]
: []
});
for (let [regex, action] of actions) {
const lastMessage = event.payload.messages[event.payload.messages.length - 1].content;
const match = lastMessage?.match(regex);
if (match) return await action(match, event);
}
return { type: 'CONTINUE' }
};
Frontend Files (React Frontend)
Frontend/contentRender.js
import React, { useEffect, useState } from "react";
import {Button, Chip, Tooltip, ThemeProvider, createTheme} from '@mui/material';
import {Autorenew, TravelExplore, Preview, HourglassEmpty, CallMade, EditNote, Check} from '@mui/icons-material';
const style = document.createElement('style');
style.innerHTML = `
.codeContainer, .codeContainer code {
background-color: #0d0d0d !important;
color: white !important;
text-shadow: none !important;
border-radius: 10px !important;
font-size: 13px !important;
}
.codeContainer * {
background-color: #0d0d0d !important;
}
`;
document.head.appendChild(style);
const Header = ({ setRenderSettings }) => {
useEffect(() => {
setRenderSettings({
disableCodeExecuteButton: true,
inputLabelsQuickSend: true,
});
}, [setRenderSettings]);
};
const isMobile = window.openkbs.isMobile;
const ChatMessageRenderer = ({ content, CodeViewer, setInputValue, sendButtonRippleRef }) => {
const [addedSuggestions, setAddedSuggestions] = useState([]);
const handleSuggestionClick = (suggestion) => {
setInputValue((prev) => prev + (prev ? '\n' : '') + suggestion);
setAddedSuggestions((prev) => [...prev, suggestion]);
setTimeout(() => sendButtonRippleRef?.current?.pulsate(), 100);
};
const output = [];
let language = null;
content.split('\n').forEach(line => {
const writeFileMatch = /writeFile\s+(?<filePath>[^\s]+)/.exec(line);
const codeStartMatch = /```(?<language>\w+)/g.exec(line);
const commandMatch = /\/(?<command>googleSearch|webpageToText|viewImage|metaAction|suggestion|jobCompleted|jobFailed)\((?<args>[^()]*)\)/g.exec(line);
if (!language && codeStartMatch) {
language = codeStartMatch.groups.language;
output.push({ language, code: '' });
} else if (language && line.match(/```/)) {
language = null;
} else if (language) {
output[output.length - 1].code += line + '\n';
} else if (commandMatch || writeFileMatch) {
const command = commandMatch?.groups?.command || 'writeFile';
let args = commandMatch?.groups?.args || writeFileMatch?.groups?.filePath;
if (args?.startsWith('"') && args?.endsWith('"')) args = args.slice(1, -1); // remove quotes if any
output.push({ command, args, line });
} else {
output.push(line);
}
});
return <ThemeProvider theme={() => createTheme(window.openkbsTheme)}>
{output.map((o, i) => {
if (typeof o === 'string') {
return <p key={i} style={{ marginTop: '0px', marginBottom: '0px' }}>{o}</p>;
} else if (o.command) {
if (o.command === 'suggestion') {
const added = addedSuggestions?.includes(o.args);
return (
<div key={`a${i}`}>
<Button
variant="contained"
color="primary"
disabled={added}
onClick={() => handleSuggestionClick(o.args)}
style={{ margin: '5px', textTransform: 'none' }}
>
{added ? <Check fontSize="small" sx={{mr: 2}} /> : ''}{o.args}
</Button>
</div>
);
} else if (o.command) {
const argsIcons = {
'execute_and_wait': <HourglassEmpty />,
'execute_and_callback': <Autorenew />
};
const commandIcons = {
'googleSearch': <TravelExplore />,
'webpageToText': <Preview />,
'writeFile': <EditNote />
};
const icon = argsIcons[o.args] || commandIcons[o.command];
return <div style={{ marginTop: '5px', marginBottom: '5px' }}>
<Tooltip title={o.line} placement="right">
<Chip
sx={{mt: '10px'}}
icon={icon}
label={o.args}
variant="outlined"
deleteIcon={ <CallMade
style={{
fontSize: 12,
borderRadius: '50%',
padding: '4px',
}}
/> }
onDelete={() => {}}
/>
</Tooltip>
</div>
}
} else {
return (
<div key={i}>
<CodeViewer
limitedWidth={isMobile}
language={o.language}
className="codeContainer"
source={o.code}
/>
</div>
);
}
})}
</ThemeProvider>
};
const onRenderChatMessage = async (params) => {
const { content } = params.messages[params.msgIndex];
const { CodeViewer, setInputValue, sendButtonRippleRef } = params;
if (content.match(/```/) || content.match(/\/(?<command>\w+)\(([\s\S]*)\)/g)) {
return (
<ChatMessageRenderer
content={content}
CodeViewer={CodeViewer}
setInputValue={setInputValue}
sendButtonRippleRef={sendButtonRippleRef}
/>
);
}
};
const exports = { onRenderChatMessage, Header };
window.contentRender = exports;
export default exports;
Frontend/contentRender.json
{
"dependencies": {
"react": "^18.2.0 (fixed)",
"react-dom": "^18.2.0 (fixed)",
"@mui/material": "^5.16.1 (fixed)",
"@mui/icons-material": "^5.16.1 (fixed)"
}
}