App Icon

WP Customer

AI agent designed to provide customer assistance to WordPress website visitors


Install
MIT LicenseGitHub Repository


Configuration

After installing the Agent through the OpenKBS WordPress plugin, follow these steps to configure the key features:

Enable Semantic Search:

Semantic Search creates and maintains embeddings of your WordPress content, enabling AI-powered search capabilities across your site.
1

Navigate to WordPress → OpenKBS → Settings → Applications → WP Customer

2Enable "Semantic Search Indexing" and click "Save All"
3Click "Start Indexing" to generate embeddings for your existing WordPress content

Enable Chat Widget:

The chat widget integrates automatically and requires Semantic Search, enabling the LLM AI Model to search your website data and assist clients with real-world actions.

1

Go to WordPress → OpenKBS → Settings → Applications → WP Customer

2Enable the "Website Chat Widget" and click "Save All"
3Open your WordPress homepage and test the chat widget

Enable Search Widget (optional):

Enables a public search endpoint allowing website visitors to perform semantic searches through a widget interface.
1

Go to OpenKBS → Settings → Public Search API

2Enable "Public Search API"
3Copy the provided Shortcode
4

Navigate to Appearance → Widgets

5Add a "Shortcode" widget to your WordPress header widget area

Customization

1. Install OpenKBS CLI:
npm install -g openkbs
2. Create and enter project directory:
mkdir my-agent && cd my-agent
3. Clone your app locally:
openkbs login
openkbs ls
openkbs clone <id-from-ls-output>
4. Initialize Git repository:
git init && git stage . && git commit -m "First commit"
5. Add new features:
Examples:
openkbs modify "Implement new command getBitcoinPrice(days) that returns price history from CoinGecko API" src/Events/actions.js
openkbs modify "Add instructions for getBitcoinPrice" src/Events/actions.js app/instructions.txt
6. Review changes and deploy to OpenKBS:
git diff
openkbs push
7. Test your customization:
Go to chat and ask: "Analyze the price of Bitcoin"

Disclaimer

The applications provided through OpenKBS are developmental blueprints and are intended solely as starting points for software engineers and developers. These open-source templates are not production-ready solutions.

Before any production deployment, developers must:

  • Conduct comprehensive code reviews and security audits
  • Implement robust security measures and safeguards
  • Perform extensive testing procedures
  • Ensure full compliance with applicable regulations
  • Adapt and enhance the codebase for specific use cases

NO WARRANTY DISCLAIMER: These blueprints are provided "as-is" without any warranties, whether express or implied. By using these blueprints, you assume full responsibility for all aspects of development, implementation, and maintenance of any derived applications. OpenKBS shall not be liable for any damages or consequences arising from the use of these blueprints.


Instructions and Source Code

You are an AI Customer Assistant for a WordPress website.
Use "we" and "our" in all responses. Keep answers brief and speak as a true team member of our business.
Execute search commands silently and respond only with relevant findings.
Skip explaining your processes.
Don't narrate your actions or thought process.
Keep all technical operations invisible and provide only definitive, meaningful, concise, accurate, and clear answers to customer.

Flow:
Before responding to the user, first call the wpSearch command and wait for a system response to get acquainted with the actual content of the site.
To render any items found with wpSearch, you can use the renderPostCard function.

Available API commands:

Guidelines:
- to execute a command, output the command in the chat, and wait for a system response in the next message
- you can output multiple commands at once
- respond to the user only based on the actual content extracted from the site, if none exists, say you cannot help
- Don't make assumptions
- Don't guess
- Do only what was asked of you.


/renderPostCard("title", "url", "imageUrl", "$29.99")
Description: """
Renders a WordPress post as a card.

Parameters:
1. title (required) - Post title
2. url (required) - Post URL
3. imageUrl (optional) - Featured image URL
4. price (optional) - Product price if post type is product
"""

/navigate("url")
Description: """
Navigates the user to a different page within the same website.
The URL must be from the same domain for security reasons.
Example usage:
/navigate("/products/t-shirts")
/navigate("/checkout")
"""

/click("selector")
Description: """
Triggers a click on the specified element on the page.
Uses CSS selectors to identify the element to click.
Example usage:
/click("button[name='add-to-cart']") - Once you have navigated to the product page, this command will add it to your cart.
"""

/wpSearch("jacket", 10, "product", 29.99)
Description: """
Search WordPress content and returns any matching posts with their title, excerpt, URL, image, and price (if product type)
Parameters:
1. query (required) - Search term
2. limit (optional) - Number of results to return (defaults to 10)
3. itemTypes (optional) - Comma-separated list of post types to search (e.g., "post,page,product", searches all if unspecified)
4. maxPrice (optional) - Maximum price as float (e.g., 29.99) (only if post type is product)
"""

/orders(1)
Description: """
Get user's WooCommerce orders with all products, shipping and billing information
"""

/order/[orderId]/shipping/[field]/[value]/
Description: """
Update shipping details for a specific order.

Example usage:
/order/123/shipping/phone/1234567890/
/order/123/shipping/address_1/New Street 123/
"""

/webpageToText("URL")
Description: """
Use this API to extract a website to text.
"""


/documentToText("documentURL")
Description: """
Extracts text from document URL - csv, text, pdf, docx, doc, xls, xlsx, etc.
"""


/imageToText("imageURL")
Description: """
Extracts text from images and returns the result (OCR).
"""


/textToSpeech("en-US", "text to convert to speech")
Description: """
The first parameter is the language code of the voice, following the BCP-47 standard.
This function converts text to speech and plays it for the user.
"""

Events Files (Node.js Backend)

Events/actions.js

export const getActions = (meta) => {
    const wpUrl = '{{secrets.wpUrl}}';
    const headers = { 'WP-API-KEY': '{{secrets.wpapiKey}}' };

    return [
        [/\/order\/(\d+)\/shipping\/([^\/]+)\/([^\/]+)/, async (match) => {
            try {
                const [_, orderId, field, value] = match;

                const orderCheck = await axios.get(`${wpUrl}/wp-json/wc/v3/orders/${parseInt(orderId)}`, {
                    headers,
                    params: {
                        customer: '{{variables.publicUserId}}'
                    }
                });

                if (!orderCheck.data || orderCheck.data.customer_id.toString() !== '{{variables.publicUserId}}') {
                    return { error: 'Order not found or access denied' }
                }

                if (orderCheck.data.status !== 'processing') {
                    return { error: 'Order shipping details can only be modified when order is processing' }
                }

                const response = await axios.put(`${wpUrl}/wp-json/wc/v3/orders/${orderId}`, {
                    shipping: {
                        [field]: value
                    }
                }, { headers });

                return { data: { id: response.data.id, shipping: response.data.shipping } }

            } catch (error) {
                return { error: 'Failed to update shipping details: ' + error.message }
            }
        }],

        [/\/?orders\s*(\d+)?/, async (match) => {
            try {
                const limit = match[1] ? parseInt(match[1]) : 1;

                const response = await axios.get(`${wpUrl}/wp-json/wc/v3/orders`, {
                    headers,
                    params: {
                        customer: '{{variables.publicUserId}}',
                        per_page: limit,
                        orderby: 'date',
                        order: 'desc'
                    }
                });

                const cleanOrders = response.data.map(({
                                                           id, status, date_created, total, currency,
                                                           payment_method, shipping_total, line_items,
                                                           shipping, billing
                                                       }) => ({
                    id, status, date_created, total, currency,
                    payment_method, shipping_total,
                    line_items: line_items.map(({ name, quantity, total, price, sku }) =>
                        ({ name, quantity, total, price, sku })),
                    shipping,
                    billing
                }));

                return { data: cleanOrders }

            } catch (error) {
                return { error: 'Failed to fetch orders: ' + error.message }
            }
        }],

        [/\/?wpSearch\("([^"]*)"(?:\s*,\s*(\d+))?(?:\s*,\s*"([^"]*)")?(?:\s*,\s*(\d+(?:\.\d+)?))?\)/, async (match) => {
            const query = match[1] || " ";
            const limit = match[2] || 10;
            const itemTypes = match[3] ? match[3].split(',').map(type => type.trim()) : undefined;
            const maxPrice = match[4];

            try {
                const params = {
                    query,
                    kbId: openkbs.kbId,
                    limit,
                    ...(itemTypes && { itemTypes }),
                    ...(maxPrice && { maxPrice })
                };

                const response = await axios.get(`${wpUrl}/wp-json/openkbs/v1/search`, {
                    headers,
                    params
                });

                return { data: response.data };
            } catch (e) {
                return { error: e.response?.data || e.message };
            }
        }],

        [/\/?webpageToText\("(.*)"\)/, async (match) => {
            try {
                let response = await openkbs.webpageToText(match[1]);

                // limit output length
                if (response?.content?.length > 5000) {
                    response.content = response.content.substring(0, 5000);
                }

                return { data: response };
            } catch (e) {
                return { error: e.response.data };
            }
        }],

        [/\/?documentToText\("(.*)"\)/, async (match) => {
            try {
                let response = await openkbs.documentToText(match[1]);

                // limit output length
                if (response?.text?.length > 5000) {
                    response.text = response.text.substring(0, 5000);
                }

                return { data: response };
            } catch (e) {
                return { error: e.response.data };
            }
        }],

        [/\/?imageToText\("(.*)"\)/, async (match) => {
            try {
                let response = await openkbs.imageToText(match[1]);

                if (response?.detections?.[0]?.txt) {
                    response = { detections: response?.detections?.[0]?.txt };
                }

                return { data: response };
            } catch (e) {
                return { error: e.response.data };
            }
        }],

    ];
}

Events/onRequest.js

import {getActions} from './actions.js';

export const handler = async (event) => {
    const meta = {};
    const actions = getActions(meta);
    const lastMessage = event.payload.messages[event.payload.messages.length - 1].content;

    const matchingActions = actions.reduce((acc, [regex, action]) => {
        const matches = [...lastMessage.matchAll(new RegExp(regex, 'g'))];
        matches.forEach(match => {
            acc.push(action(match));
        });
        return acc;
    }, []);

    if (matchingActions.length > 0) {
        try {
            const results = await Promise.all(matchingActions);
            return {
                type: 'RESPONSE',
                data: results,
                ...meta
            };
        } catch (error) {
            return {
                type: 'ERROR',
                error: error.message,
                ...meta
            };
        }
    }

    return { type: 'CONTINUE' };
};

Events/onResponse.js

import {getActions} from './actions.js';

export const handler = async (event) => {
    const meta = {_meta_actions: ["REQUEST_CHAT_MODEL"]};
    const actions = getActions(meta);
    const lastMessage = event.payload.messages[event.payload.messages.length - 1].content;

    const matchingActions = actions.reduce((acc, [regex, action]) => {
        const matches = [...lastMessage.matchAll(new RegExp(regex, 'g'))];
        matches.forEach(match => {
            acc.push(action(match));
        });
        return acc;
    }, []);

    if (matchingActions.length > 0) {
        try {
            const results = await Promise.all(matchingActions);
            return {
                type: 'RESPONSE',
                data: results,
                ...meta
            };
        } catch (error) {
            return {
                type: 'ERROR',
                error: error.message,
                ...meta
            };
        }
    }

    return { type: 'CONTINUE' };
};

Frontend Files (React Frontend)

Frontend/PostCard.js

import React from "react";
import {

    Card,
    CardContent,
    CardMedia,
    Typography,
    CardActionArea,
    Box
} from '@mui/material';
import {CallMade} from '@mui/icons-material';

const PostCard = ({ title, url, imageUrl, price }) => {
    return (
        <Card sx={{
            maxWidth: 345,
            margin: '10px 0',
            width: '100%',
            backgroundColor: '#f8f9fa'
        }}>
            <CardActionArea onClick={() => window.open(url, '_blank')}>
                {imageUrl && (
                    <CardMedia
                        component="img"
                        height="140"
                        image={imageUrl}
                        alt={title}
                        sx={{
                            objectFit: 'contain',
                            backgroundColor: '#ffffff'
                        }}
                    />
                )}
                <CardContent>
                    <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
                        <Typography
                            variant="subtitle1"
                            component="div"
                            sx={{
                                fontWeight: 500,
                                flex: 1,
                                lineHeight: 1.3,
                                overflow: 'hidden',
                                display: '-webkit-box',
                                WebkitLineClamp: 3,
                                WebkitBoxOrient: 'vertical',
                            }}
                        >
                            {title}
                        </Typography>
                        <CallMade
                            sx={{
                                ml: 1,
                                fontSize: 16,
                                color: 'text.secondary',
                                flexShrink: 0
                            }}
                        />
                    </Box>
                    {price && (
                        <Typography
                            variant="body1"
                            color="primary"
                            sx={{
                                fontWeight: "bold",
                                mt: 1
                            }}
                        >
                            {price}
                        </Typography>
                    )}
                </CardContent>
            </CardActionArea>
        </Card>
    );
};

export default PostCard;

Frontend/contentRender.js

import React, { useEffect, useState, useRef } from "react";
import {
    Chip,
    Tooltip,
    ThemeProvider,
    createTheme,
} from '@mui/material';
import { Search, Preview, CallMade } from '@mui/icons-material';
import PostCard from "./PostCard.js";
import {
    executeCommand,
    isMessageExpired,
    cleanupExecutedCommands,
    isCommandExecuted,
    markCommandAsExecuted
} from "./utils";


const Header = ({ setRenderSettings }) => {
    useEffect(() => {
        setRenderSettings({
            disableCodeExecuteButton: true,
            inputLabelsQuickSend: true,
            disableChatModelsSelect: true
        });
    }, [setRenderSettings]);
};

const ChatMessageRenderer = ({ content, msgId, kbId }) => {
    const [executionInProgress, setExecutionInProgress] = useState(false);
    const timeoutId = useRef(null);

    useEffect(() => {
        const executeCommands = async () => {
            if (isMessageExpired(msgId) || executionInProgress) return;
            cleanupExecutedCommands();

            // Parse commands from content
            const lines = content.split('\n');
            const commands = lines
                .map((line, index) => {
                    const navigateMatch = /\/navigate\("([^"]*)"\)/g.exec(line);
                    const clickMatch = /\/click\("([^"]*)"\)/g.exec(line);

                    if (navigateMatch) {
                        return { type: 'navigate', args: { url: navigateMatch[1] }, index };
                    }
                    if (clickMatch) {
                        return { type: 'click', args: { selector: clickMatch[1] }, index };
                    }
                    return null;
                })
                .filter(cmd => cmd !== null);

            if (commands.length === 0) return;

            setExecutionInProgress(true);

            // Execute commands sequentially
            for (const command of commands) {
                if (!isCommandExecuted(msgId, command.index)) {
                    try {
                        markCommandAsExecuted(msgId, command.index);
                        await executeCommand(command.type, command.args, kbId);
                    } catch (error) {
                        console.error('Command execution failed:', error);
                    }
                }
            }

            setExecutionInProgress(false);
        };

        // Clear any existing timeout
        if (timeoutId.current) {
            clearTimeout(timeoutId.current);
        }

        // Postpone execution until LLM completes content generation
        timeoutId.current = setTimeout(executeCommands, 1000);

        return () => {
            if (timeoutId.current) {
                clearTimeout(timeoutId.current);
            }
        };
    }, [content, msgId, kbId, executionInProgress]);

    const output = [];
    content.split('\n').forEach(line => {
        const renderPostMatch = /\/renderPostCard\("([^"]*)",\s*"([^"]*)"(?:,\s*"([^"]*)")?(?:,\s*"([^"]*)")?\)/g.exec(line);
        const commandMatch = renderPostMatch || /\/(?<command>wpSearch|webpageToText|documentToText|imageToText|navigate|click)\((?<args>[^()]*)\)/g.exec(line);

        if (commandMatch) {
            const command = renderPostMatch ? 'renderPostCard' : commandMatch.groups?.command;
            const args = renderPostMatch
                ? commandMatch.slice(1).filter(Boolean).map(arg => `"${arg}"`).join(', ')
                : commandMatch.groups?.args;
            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) {
            const commandIcons = {
                'wpSearch': <Search />,
                'webpageToText': <Preview />,
            };

            if (o.command === 'wpSearch') o.args = o.args.match(/"([^"]*)"/)[1] // render only search query

            if (o.command === 'renderPostCard') {
                const [title, url, imageUrl, price] = o.args
                    .split(',')
                    .map(arg => arg.trim().replace(/^"|"$/g, ''));

                return (
                    <div key={i} style={{ marginTop: '5px', marginBottom: '5px' }}>
                        <PostCard
                            title={title}
                            url={url}
                            imageUrl={imageUrl}
                            price={price}
                        />
                    </div>
                );
            }

            const icon = 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>

        }
    })}
    </ThemeProvider>
};

const onRenderChatMessage = async (params) => {
    const { content, msgId } = params.messages[params.msgIndex];
    const { setInputValue, sendButtonRippleRef, KB } = params;

    if (content.match(/\/(?<command>\w+)\(([\s\S]*)\)/g)) {
        return (
            <ChatMessageRenderer
                content={content}
                msgId={msgId}
                kbId={KB?.kbId}
                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)",
    "@emotion/react": "^11.10.6 (fixed)",
    "@emotion/styled": "^11.10.6 (fixed)"
  }
}

Frontend/utils.js

const COMMAND_EXPIRY_TIME = 1 * 60 * 1000;
const COMMAND_DELAY = 2000;

export const getMessageTimestamp = (msgId) => {
    return parseInt(msgId.split('-')[0]);
};

export const isMessageExpired = (msgId) => {
    const timestamp = getMessageTimestamp(msgId);
    return Date.now() - timestamp > COMMAND_EXPIRY_TIME;
};

const getExecutedCommands = () => {
    return JSON.parse(localStorage.getItem('executedCommands') || '{}');
};

export const cleanupExecutedCommands = () => {
    const executedCommands = getExecutedCommands();
    const now = Date.now();

    const cleanedCommands = Object.entries(executedCommands).reduce((acc, [msgId, commands]) => {
        if (now - getMessageTimestamp(msgId) <= COMMAND_EXPIRY_TIME) {
            acc[msgId] = commands;
        }
        return acc;
    }, {});

    localStorage.setItem('executedCommands', JSON.stringify(cleanedCommands));
};

export const isCommandExecuted = (msgId, commandIndex) => {
    const executedCommands = getExecutedCommands();
    return executedCommands[msgId]?.includes(commandIndex);
};

export const markCommandAsExecuted = (msgId, commandIndex) => {
    const executedCommands = getExecutedCommands();
    if (!executedCommands[msgId]) {
        executedCommands[msgId] = [];
    }
    executedCommands[msgId].push(commandIndex);
    localStorage.setItem('executedCommands', JSON.stringify(executedCommands));
};

// Modified command execution
export const executeCommand = (command, args, kbId) => {
    return new Promise((resolve) => {
        const commandMessage = {
            type: 'openkbsCommand',
            command: command,
            kbId,
            ...args
        };
        window.parent.postMessage(commandMessage, '*');
        setTimeout(resolve, COMMAND_DELAY);
    });
};