In this step-by-step guide, we will build a full-stack application that uses WP Engine’s AI Toolkit, Retrieval Augmented Generation (RAG), and Google Gemini to deliver accurate and contextually relevant responses in a chatbot within a Next.js framework.
Before we discuss the technical steps, let’s review the tools and techniques we will use.
Table of Contents
- RAG
- WP Engine’s AI Toolkit
- Google Gemini API (AI API’s)
- Prerequisites
- Make Requests to the WP Engine Smart Search API
- Creating the R in RAG
- API Endpoint for Chat UI
- Create UI Components for Chat Interface
- Messages Component
- Chat Input Component
- Update the page.tsx template
- Update the layout.tsx file with metadata
- Test the ChatBot
- Conclusion
RAG
Retrieval-augmented generation (RAG) is a technique that enables AI models to retrieve and incorporate new information.
It modifies interactions with a large language model (LLM) so that the model responds to user queries with reference to a specified set of documents, using this information to supplement information from its pre-existing training data. This allows LLMs to use domain-specific and/or updated information.
Our use case in this article will include providing chatbot access to our data from Smart Search.
WP Engine’s AI Toolkit
Here’s an overview of WP Engine’s AI Toolkit and the core capabilities it brings to both traditional and headless WordPress sites:
- Smart Search & AI-Powered Hybrid Search
At its heart, the AI Toolkit includes WP Engine Smart Search—a drop-in replacement for WordPress’s native search that’s typo-tolerant, weight-aware, and ultra-fast. Out of the box, you get three modes: Full-Text (stemming and fuzzy matching), Semantic (NLP-driven meaning over mere keywords), and Hybrid (a tunable blend of both). Behind the scenes, Smart Search automatically indexes your Posts, Pages, Custom Post Types, ACF fields, WooCommerce products, and more—so you can serve richer, more relevant results without writing a line of search logic yourself.
- Vector Database, Fully Managed
You don’t need to stand up or scale your own vector store—WP Engine’s AI Toolkit manages that for you. As new content is published or edited, the plugin streams updates in real time to its vector database. Queries are encoded into embeddings, nearest-neighbor lookups happen in milliseconds, and the freshest site content is always just a search away. This under-the-hood Vector DB also powers the AI aspects of Hybrid Search, ensuring that semantic similarity and context ranking work against live data.
- Headless Integration
For sites using WP Engine’s Headless Platform, all of these features—Smart Search querying, vector indexing, AI-powered hybrid ranking, and recommendations—are exposed through GraphQL. The AI Toolkit installs and configures both WPGraphQL and Smart Search automatically, so your front-end app can orchestrate retrieval and generation without extra middleware.
- Recommendations
An AI-driven content discovery feature that helps you surface “Related” or “Trending” posts (or custom post types) anywhere on your site—whether you’re using the Gutenberg editor or building a headless front end via WPGraphQL.
Google Gemini API (AI API’s)
The Google Gemini API offers developers a powerful and versatile interface to access Google’s state-of-the-art Gemini AI models. These multimodal models are designed to seamlessly understand and generate content across various data types, including text, code, images, audio, and video.
For our chatbot integration, the Gemini API provides advanced natural language understanding, allowing it to interpret user queries and generate human-like responses. It supports multi-turn conversations, maintaining context over extended interactions, which is crucial for building engaging and intelligent conversational experiences. We will leverage the API’s flexibility to customize chatbot behavior, tone, and style, enabling a wide range of use cases from customer service to creative content generation.
Prerequisites
To benefit from this article, you should be familiar with the basics of working with the command line, headless WordPress development, Next.js, and the WP Engine User Portal.
Steps for setting up:
1. Set up an account on WP Engine and get a WordPress install running.
2. Add a Smart Search license. Refer to the docs here for adding a license.
3. Navigate to the WP Admin of your install. Inside your WP Admin, go to WP Engine Smart Search > Settings
. You will find your Smart Search URL and access token here. Copy and save it. We will need it later. You should see this page:

4. Next, navigate to Configuration
, select the Hybrid
card, and add the post_content
field in the Semantic settings
section. We are going to use this field as our AI-powered field for similarity searches. Make sure to hit Save Configuration
afterward.
5. After saving the configuration, head on over to the Index data
page, then click Index Now
. It will give you this success message once completed :
6. Create an API account on Google Gemini (Or whatever AI model you choose, e.g., OpenAI API). Once created, navigate to your project’s dashboard. If you are using the Gemini API, go to the Google AI Studio. In your project’s dashboard, go to API Keys. You should see a page like this:
Generate a new key, copy, and save your API key because we will need this later. The API key is free on Google Gemini, but the free tier has limits.
7. Head over to your terminal or CLI and create a new Next.js project by pasting this utility command in:
npx create-next-app@latest name-of-your-app
Code language: CSS (css)
You will receive prompts in the terminal asking you how you want your Next.js app scaffolded. Answer them accordingly:
Would you like to use TypeScript? Yes
Wold you like to use ESLint? Yes
Would you like to use Tailwind CSS? Yes
Would you like to use the `src/` directory? Yes
Would you like to use App Router? Yes
Would you like to customize the default import alias (@/*)? No
Code language: JavaScript (javascript)
Once your Next.js app is created, you will need to install the dependencies needed to ensure our app works. Copy and paste this command in your terminal:
npm install @ai-sdk/google ai openai-edge react-icons react-markdown
Code language: CSS (css)
Once the Next project is done scaffolding, cd
into the project and then open up your code editor.
8. In your Next.js project, create a .env.local
file with the following environment variables:
GOOGLE_GENERATIVE_AI_API_KEY="<your key here>" # if you chose another AI model, you can name this key whatever you want
SMART_SEARCH_URL="<your smart search url here>"
SMART_SEARCH_ACCESS_TOKEN="<your smart search access token here>"
Code language: PHP (php)
Here is the link to the final code repo so you can check step by step and follow along.
Make Requests to the WP Engine Smart Search API
The first thing we need to do is set up the request to the Smart Search API using the Similarity query. Create a file in the src/app
directory called utils/context.ts
. Copy the code below and paste it into that file:
// These are the types that are used in the `getContext` function
type Doc = {
id: string;
data: Record<string, unknown>;
score: number;
};
type Similarity = {
total: number;
docs: Doc[];
};
export type GraphQLSimilarityResponse = {
data: {
similarity: Similarity;
};
errors?: { message: string }[];
};
const QUERY = /* GraphQL */ `
query GetContext($message: String!, $field: String!) {
similarity(
input: { nearest: { text: $message, field: $field } }
) {
total
docs {
id
data
score
}
}
}
`;
export const getContext = async (
message: string,
): Promise<GraphQLSimilarityResponse> => {
const url = process.env.SMART_SEARCH_URL;
const token = process.env.SMART_SEARCH_ACCESS_TOKEN;
if (!url || !token) {
throw new Error(
"SMART_SEARCH_URL and SMART_SEARCH_ACCESS_TOKEN must be defined.",
);
}
const res = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
query: QUERY,
variables: { message, field: "post_content" } as const,
}),
});
if (!res.ok) {
throw new Error(`Smart Search responded with ${res.status} ${res.statusText}`);
}
return res.json() as Promise<GraphQLSimilarityResponse>;
};
Code language: JavaScript (javascript)
This block defines TypeScript types (Doc, Similarity, and Response
) to model the shape of a similarity‐search GraphQL response, and exports an async getContext
function that performs the actual lookup. Inside getContext
, it reads the Smart Search endpoint URL and access token from environment variables, then constructs a GraphQL query named GetContext
that requests the nearest documents (by embedding similarity) for a given message
against a specified field (“post_content”
).
It sends that query and its variables in the body of a POST request—complete with JSON content headers and a Bearer authorization header—to the Smart Search API endpoint, and finally returns the parsed JSON result. By encapsulating the fetch logic and typing the response, this function provides a clean, reusable way to retrieve semantically related WordPress content for use in a RAG‐style chatbot.
Creating the “R” in RAG
The next file we need to create is the “Retrieval” portion in our RAG pipeline. Create a tools.ts
file in the utils
folder and copy and paste this code block:
import { tool } from "ai";
import { z } from "zod";
import { getContext } from "@/app/utils/context";
// Define the search tool
export const smartSearchTool = tool({
description:
"Search for information about TV shows using WP Engine Smart Search. Use this to answer questions about TV shows, their content, characters, plots, etc., when the information is not already known.",
parameters: z.object({
query: z
.string()
.describe(
"The search query to find relevant TV show information based on the user's question."
),
}),
execute: async ({ query }: { query: string }) => {
console.log(`[Tool Execution] Searching with query: "${query}"`);
try {
const context = await getContext(query);
if (context.errors && context.errors.length > 0) {
console.error(
"[Tool Execution] Error fetching context:",
context.errors
);
// Return a structured error message that the LLM can understand
return {
error: `Error fetching context: ${context.errors[0].message}`,
};
}
if (
!context.data?.similarity?.docs ||
context.data.similarity.docs.length === 0
) {
console.log("[Tool Execution] No documents found for query:", query);
return {
searchResults: "No relevant information found for your query.",
};
}
const formattedResults = context.data.similarity.docs.map((doc) => {
if (!doc) {
return {};
}
return {
id: doc.id,
title: doc.data.post_title,
content: doc.data.post_content,
url: doc.data.post_url,
categories: doc.data.categories.map((category: any) => category.name),
searchScore: doc.score,
};
});
// console.log("[Tool Execution] Search results:", formattedResults);
return { searchResults: formattedResults }; // Return the formatted string
} catch (error: any) {
console.error("[Tool Execution] Exception:", error);
return { error: `An error occurred while searching: ${error.message}` };
}
},
});
export const weatherTool = tool({
description:
"Get the current weather information for a specific location. Use this to answer questions about the weather in different cities.",
parameters: z.object({
location: z
.string()
.describe(
"The location for which to get the current weather information."
),
}),
execute: async ({ location }: { location: string }) => {
console.log(`[Tool Execution] Getting weather for location: "${location}"`);
try {
// Simulate fetching weather data
const weatherData = {
location,
temperature: "22°C",
condition: "Sunny",
humidity: "60%",
windSpeed: "15 km/h",
};
const formattedWeather = `The current weather in ${weatherData.location} is ${weatherData.temperature} with ${weatherData.condition}. Humidity is at ${weatherData.humidity} and wind speed is ${weatherData.windSpeed}.`;
return { weather: formattedWeather };
} catch (error: any) {
console.error("[Tool Execution] Exception:", error);
return {
error: `An error occurred while fetching weather data: ${error.message}`,
};
}
},
});
Code language: JavaScript (javascript)
This module registers two “tools” with the AI SDK—one for performing semantic searches against your WP Engine Smart Search index and another for fetching (simulated) weather data. The smartSearchTool
uses Zod to validate a single query string, then calls your getContext
helper to run a similarity‐search GraphQL request; it handles errors or empty results gracefully, formats any returned documents (including ID, title, content, URL, categories, and relevance score), and exposes them as a structured searchResults
array.
The weatherTool
declares a location parameter, simulates a lookup of current conditions (temperature, humidity, wind speed), and returns a human‐readable summary. By wrapping each in the tool() factory—complete with descriptions, parameter schemas, and execute functions—this file makes both search and weather functionality available for the LLM to invoke during a conversation.
API Endpoint for Chat UI – The AG in RAG
Next, let’s create the chat endpoint for the Chat UI, which is the AG in RAG. In the src/app
directory, create a api/chat/
subfolder, then add a route.ts
file in there. Copy and paste this code into the file:
// IMPORTANT! Set the runtime to edge
export const runtime = "edge";
import { convertToCoreMessages, Message, streamText } from "ai";
import { createGoogleGenerativeAI } from "@ai-sdk/google";
import { smartSearchTool, weatherTool } from "@/app/utils/tools";
/**
* Initialize the Google Generative AI API
*/
const google = createGoogleGenerativeAI();
export async function POST(req: Request) {
try {
const { messages }: { messages: Array<Message> } = await req.json();
const coreMessages = convertToCoreMessages(messages);
const smartSearchPrompt = `
- You can use the 'smartSearchTool' to find information relating to tv shows.
- WP Engine Smart Search is a powerful tool for finding information about TV shows.
- After the 'smartSearchTool' provides results (even if it's an error or no information found)
- You MUST then formulate a conversational response to the user based on those results but also use the tool if the users query is deemed plausible.
- If search results are found, summarize them for the user.
- If no information is found or an error occurs, inform the user clearly.`;
const systemPromptContent = `
- You are a friendly and helpful AI assistant
- You can use the 'weatherTool' to provide current weather information for a specific location.
- Do not invent information. Stick to the data provided by the tool.`;
const response = streamText({
model: google("models/gemini-2.0-flash"),
system: [smartSearchPrompt, systemPromptContent].join("\n"),
messages: coreMessages,
tools: {
smartSearchTool,
weatherTool,
},
onStepFinish: async (result) => {
// Log token usage for each step
if (result.usage) {
console.log(
`[Token Usage] Prompt tokens: ${result.usage.promptTokens}, Completion tokens: ${result.usage.completionTokens}, Total tokens: ${result.usage.totalTokens}`
);
}
},
maxSteps: 5,
});
// Convert the response into a friendly text-stream
return response.toDataStreamResponse({});
} catch (e) {
throw e;
}
}
Code language: JavaScript (javascript)
This file defines an Edge‐runtime POST endpoint that wires up Google’s Gemini model with two custom tools—smartSearchTool
for TV-show lookups via WP Engine Smart Search and weatherTool
for fetching current weather. When a request arrives, it parses the incoming chat messages, converts them into the AI SDK’s core message format, and assembles two system‐level prompts: one describing how to use the search tool, the other explaining the weather tool.
It then invokes streamText
with the Gemini “flash” model, the combined system prompt, the user’s message history, and the tool definitions, allowing the LLM to call out to those tools during generation. A callback logs token usage after each reasoning step (up to five steps), and the function finally returns the AI’s response as a streamed HTTP response.
Create UI Components for Chat Interface
The Chat.tsx
file
Now, let’s create the chat interface. In the src/app
directory, create a components
folder. Then create a Chat.tsx
file. Copy and paste this code block in that file:
"use client";
import React, { ChangeEvent } from "react";
import Messages from "./Messages";
import { Message } from "ai/react";
import LoadingIcon from "../Icons/LoadingIcon";
import ChatInput from "./ChatInput";
interface Chat {
input: string;
handleInputChange: (e: ChangeEvent<HTMLInputElement>) => void;
handleMessageSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
messages: Message[];
status: "submitted" | "streaming" | "ready" | "error";
}
const Chat: React.FC<Chat> = ({
input,
handleInputChange,
handleMessageSubmit,
messages,
status,
}) => {
return (
<div id="chat" className="flex flex-col w-full mx-2">
<Messages messages={messages} />
{status === "submitted" && <LoadingIcon />}
<form
onSubmit={handleMessageSubmit}
className="ml-1 mt-5 mb-5 relative rounded-lg"
>
<ChatInput input={input} handleInputChange={handleInputChange} />
</form>
</div>
);
};
export default Chat;
Code language: JavaScript (javascript)
This file defines a client-side React Chat component that ties together your message list, input field, and loading indicator. It declares a Chat
props interface—containing the current input value, change and submit handlers, the array of chat messages, and a status
flag—and uses those props to control its rendering.
Inside the component, it first renders the <Messages>
list to show the conversation history. If the status is "submitted"
, it displays a <LoadingIcon>
spinner to indicate that a response is pending.
Finally, it renders a <form>
wrapping the <ChatInput>
component wired to the provided input value and change handler, so users can type and submit new messages.
Messages Component
Staying in the src/app/components
directory, create a Messages.tsx
file. Copy and paste this code block in:
import { Message } from "ai";
import { useEffect, useRef } from "react";
import ReactMarkdown from "react-markdown";
export default function Messages({ messages }: { messages: Message[] }) {
const messagesEndRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages]);
return (
<div
className="border-1 border-gray-100 overflow-y-scroll flex-grow flex-col justify-end p-1"
style={{ scrollbarWidth: "none" }}
>
{messages.map((msg, index) => (
<div
key={index}
className={`${
msg.role === "assistant" ? "bg-green-500" : "bg-blue-500"
} my-2 p-3 shadow-md hover:shadow-lg transition-shadow duration-200 flex slide-in-bottom bg-blue-500 border border-gray-900 message-glow`}
>
<div className="ml- rounded-tl-lg p-2 border-r flex items-center">
{msg.role === "assistant" ? "🤖" : "🧒🏻"}
</div>
<div className="ml-2 text-white">
<ReactMarkdown>{msg.content}</ReactMarkdown>
</div>
</div>
))}
<div ref={messagesEndRef} />
</div>
);
}
Code language: JavaScript (javascript)
The Messages component renders a scrollable list of chat messages, automatically keeping the view scrolled to the latest entry. It accepts a messages
prop (an array of Message objects) and uses a ref
to an empty <div>
at the bottom; a useEffect
hook watches for changes to the messages array and calls scrollIntoView
on that ref so new messages smoothly come into view.
Each message is wrapped in a styled <div>
whose background color and avatar icon depend on the message’s role (“assistant” vs. “user”), and the text content is rendered via ReactMarkdown
to support Markdown formatting.
Chat Input Component
Lastly, staying in the components/Chat
directory, we have the chat input. Create a ChatInput.tsx
file and copy and paste this code block in:
import { ChangeEvent } from "react";
import SendIcon from "../Icons/SendIcon";
interface InputProps {
input: string;
handleInputChange: (e: ChangeEvent<HTMLInputElement>) => void;
}
function Input({ input, handleInputChange }: InputProps) {
return (
<div className="bg-gray-800 p-4 rounded-xl shadow-lg w-full max-w-2xl mx-auto">
<input
type="text"
value={input}
onChange={handleInputChange}
placeholder={"Ask Smart Search about TV shows..."}
className="w-full bg-transparent text-gray-200 placeholder-gray-500 focus:outline-none text-md mb-3"
/>
<div className="flex">
<button
type="submit"
className="p-1 hover:bg-gray-700 rounded-md transition-colors ml-auto"
aria-label="Send message"
disabled={!input.trim()}
>
<SendIcon />
</button>
</div>
</div>
);
}
export default Input;
Code language: JavaScript (javascript)
This file exports an Input component that renders a styled text field and send button for your chat UI. It takes a input
string and an handleInputChange
callback to keep the input controlled, showing a placeholder prompt (“Ask Smart Search about TV shows…”). The send button, decorated with a SendIcon
, is disabled when the input is empty or just whitespace.
Update the page.tsx
template
We need to modify the src/app/page.tsx
file to add the Chat component to the page. In the page.tsx
file copy and paste this code:
"use client";
import Chat from "./components/Chat/Chat";
import { useChat } from "@ai-sdk/react";
import { useEffect } from "react";
const Page: React.FC = () => {
const {
messages,
input,
handleInputChange,
handleSubmit,
setMessages,
status,
} = useChat();
useEffect(() => {
if (messages.length < 1) {
setMessages([
{
role: "assistant",
content: "Welcome to the Smart Search chatbot!",
id: "welcome",
},
]);
}
}, [messages, setMessages]);
return (
<div className="flex flex-col justify-between h-screen bg-white mx-auto max-w-full">
<div className="flex w-full flex-grow overflow-hidden relative bg-slate-950">
<Chat
input={input}
handleInputChange={handleInputChange}
handleMessageSubmit={handleSubmit}
messages={messages}
status={status}
/>
</div>
</div>
);
};
export default Page;
Code language: JavaScript (javascript)
This file defines our page component that leverages the useChat
hook from the @ai-sdk/react
package to manage chat state, including messages, input text, submission handler, and status.
Upon initial render, a useEffect
hook checks if there are no messages and injects a default assistant greeting. The component returns a full-viewport flexbox layout with a styled background area in which it renders the Chat
component, passing along the chat state and handlers.
Update the layout.tsx
file with metadata
We need to add metadata to our layout. Copy and paste this code block in the src/app/layout.tsx
file:
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Smart Search RAG",
description: "Lets make a chatbot with Smart Search",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
);
}
Code language: JavaScript (javascript)
This file configures the global layout and metadata for the app: it imports global styles, loads the Inter font, and sets the page title and description. The default RootLayout
component wraps all page content in <html>
and <body>
tags, applying the Inter font’s class to the body.
CSS Note: The last thing to add for the styling is the globals.css file. Visit the code block here and copy and paste it into your project.
Test the ChatBot
The chatbot should be completed and testable in this state. In your terminal, run npm run dev
and navigate to http://localhost:3000
. Try asking the chatbot a few questions. You should see this in your browser:
Conclusion
We hope this article helped you understand how to create a chatbot with WP Engine’s AI toolkit in headless WordPress! Stay tuned for the next article on embedding this and using it in traditional WordPress!!
As always, we’re super stoked to hear your feedback and learn about the headless projects you’re working on, so hit us up in the Headless WordPress Discord!