WP Customer
AI agent designed to provide customer assistance to WordPress website visitors
Installation
/wp-content/plugins/
and activate in WordPressWP Customer
via OpenKBS menuSign up
with Google account and approve connection requestsConfiguration
Enable Semantic Search:
Navigate to WordPress → OpenKBS → Settings → Applications → WP Customer
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.
Go to WordPress → OpenKBS → Settings → Applications → WP Customer
Enable Search Widget (optional):
Go to OpenKBS → Settings → Public Search API
Navigate to Appearance → Widgets
Customization
npm install -g openkbs
mkdir my-agent && cd my-agent
openkbs login
openkbs ls
openkbs clone <id-from-ls-output>
git init && git stage . && git commit -m "First commit"
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
git diff
openkbs push
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);
});
};