App Icon

Woo Agent

AI agent designed to automate WooCommerce tasks.


Install
MIT LicenseGitHub Repository

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)"
  }
}