How to Build an AI FAQ System with Strapi, LangChain & OpenAI
Tags: react, openapi, strapi, faqapp
Published: 2024-04-21 15:24:39

## Introduction
Frequently Asked Questions (FAQs) offer users immediate access to answers for common queries. However, as the volume and complexity of inquiries grow, manual management of FAQs becomes unsupportable. This is where an AI-powered FAQ system comes in.

In this tutorial, you'll learn how to create an AI-driven FAQ system using Strapi, LangChain.js, and OpenAI. This system will allow users to pose queries related to Strapi CMS and receive accurate responses generated by a GPT model.
## Prerequisites
To comfortably follow along with this tutorial, you need to have:
- [NodeJs](https://nodejs.org/en/download/) installed in your system
- Basic knowledge of [ReactJs](https://react.dev/)
- Basic Knowledge of [Express](https://expressjs.com/)
- Basic knowledge of [LangChain.js](https://js.langchain.com/docs/get_started/introduction)

## Setting Up the Project
You need to configure the data source, which, in this case, is Strapi. Then, obtain an OpenAI API key, initialize a React project, and finally install the required dependencies.

## Configuring Strapi as the Source for Managing FAQ Data
[Strapi](https://strapi.io/) provides a centralized data managing platform. This makes it easier to organize, update, and maintain the FAQ data. It also automatically generates a RESTful API for accessing the content stored in its database.

### Install Strapi
If you don't have Strapi installed in your system, proceed to your terminal and run the following command:
```bash
npx create-strapi-app@latest my-project
```
The above command will install Strapi into your system and launch the admin registration page on your browser. 

![Admin Registration](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y13j4y0hr3lh4i97za1q.jpeg)
Fill in your credentials in order to access the Strapi dashboard.

### Create a Collection Type

On the dashboard, under **Content-Type Builder** create a new collection type and name it `FAQ`.

![New Collection Type](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rkm21dng169kz7fw9l1b.jpeg)
Then, add a `question` and an `answer` field to the **FAQ collection**. The `question` field should be of type **text** as it will be a plain text input. As for the `answer` field use **Rich Text (Blocks)** type as it allows formatted text.

![New Field](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qe70brt8erni7a9feai5.jpeg)
Proceed to the Content Manager and add entries to the    `FAQ` collection type. Each entry should have a FAQ question and its corresponding answer. Make sure you publish the entry. Create as many entries as you wish.

![Entry](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/46uhgjacriu5x7x9o91j.jpeg)
### Expose Collection API
Now that you have the `FAQ` data in Strapi, you need to expose it via an API. This will allow the application you will create to consume it.

To achieve this, proceed to ***Settings > Users & Permissions Plugin > Roles > Public***.

![API](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ckush9q17o1blon5djdp.jpeg)
Click on `Faq` Under **permissions**, check `find` and `findOne` actions and save.

![FindOne](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sjcmw0p2wiag4krs9k8z.jpeg)
This will allow us to retrieve our FAQ data via the http://localhost:1337/api/faqs endpoint. Here is how the data looks via a get request.

![Sample Data](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0gjqk0otqr8jhd7rmc8g.jpeg)
Strapi is now configured and the FAQ data is ready for use.

### Obtaining the OpenAI API Key
- Proceed to the [OpenAI API website](https://platform.openai.com/docs/overview) and create an account if you don't have one.
- Then click on API keys.

![OpenAI API Key](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/91jh55ntpxpa796bb6wn.jpeg)
- Create a new secret key. Once generated, copy and save the API key somewhere safe as you will not be able to view it again.

### Initializing a React Project and Installing the Required Dependencies
This is the final step needed to complete setting up our project. Create a new directory in your preferred location and open it with an **IDE** like **VS Code**. Then run the following command on the terminal:
```bash
npx create-react-app faq-bot
```
The command will create a new React.js application named `faq-bot` set up and ready to be developed further.

Then navigate to the `faq-bot` directory and run the following command to install all the dependencies you need to develop the **FAQ AI** application:
```bash
yarn add axios langchain @langchain/openai express cors
```
If you don't have yarn installed, install it using this command:
```bash
npm install -g yarn
```
You can use **npm** to install the dependencies, but during development, I found **yarn** to be better at handling any dependency conflict issues that occurred.

The dependencies will help you achieve the following:

- [`axios`](https://www.npmjs.com/package/axios): To fetch data from the Strapi CMS API and also to fetch responses from our Express server.
- [`langchain`](https://www.npmjs.com/package/langchain): To implement the [Retrieval Augmented Generation(RAG)](https://blogs.nvidia.com/blog/what-is-retrieval-augmented-generation/) part of the application.
- [`@langchain/openai`](https://www.npmjs.com/package/@langchain/openai): To handle communication with the OpenAI API.
- [`express`](https://www.npmjs.com/package/express): To create a simple server to serve the frontend.
- [`cors`](https://www.npmjs.com/package/cors): To ensure the server responds correctly to requests from different origins.

## Creating the FAQ AI App Backend

The core of your FAQ system will reside in an Express.js server. It will leverage the RAG (Retriever Augmented Generation) approach.

![RAG](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/azx0sodmvp9iezzpk2te.jpeg)
 RAG approach enhances the accuracy and richness of responses. It achieves this by combining information retrieval with large language models (LLMs) to provide more factually grounded answers. A retrieval locates relevant passages from external knowledge sources, such as FAQs stored in Strapi CMS. These passages, along with the user's query, are then fed into the LLM. By leveraging both internal knowledge and retrieved context, the LLM generates responses that are more informative and accurate.
  
The server will be responsible for managing incoming requests, retrieving FAQ data from Strapi, processing user queries, and utilizing RAG for generating AI-driven responses. 

### Importing the Necessary Modules and Setting Up the Server
At the root of your `faq-bot` project, create a file and name it `server.mjs`. The extension indicates that the JavaScript code is written in the **[ECMAScript module format](https://nodejs.org/api/esm.html)**. ECMAScript modules are a standard mechanism for modularizing JavaScript code.

Then open the `server.mjs` file and proceed to import the libraries we installed earlier and some specific ones from `LangChain`. Proceed to define the port on which the server will listen for incoming requests. Finally, configure the middleware functions to handle JSON parsing and CORS.
```javascript
import express from "express";
import axios from "axios";
import dotenv from "dotenv";
import cors from "cors"; 
import { ChatOpenAI } from "@langchain/openai";
import { createStuffDocumentsChain } from "langchain/chains/combine_documents";
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";
import { OpenAIEmbeddings } from "@langchain/openai";
import { MemoryVectorStore } from "langchain/vectorstores/memory";
import { createRetrievalChain } from "langchain/chains/retrieval";
import { createHistoryAwareRetriever } from "langchain/chains/history_aware_retriever";
import { MessagesPlaceholder } from "@langchain/core/prompts";
import { HumanMessage, AIMessage } from "@langchain/core/messages";
import { Document } from "langchain/document";

dotenv.config();

const app = express();
const PORT = process.env.PORT || 30080;

// Middleware to handle JSON requests
app.use(express.json());
app.use(cors()); // Add this line to enable CORS for all routes

```
You will understand what each library does as we move on with the code.

The rest of the code in the **"Creating the FAQ AI App Backend"** section will reside in the same `server.mjs` file as the code above. The code in each subsection is a continuation of the code explained in the previous subsection.
### Initializing the OpenAI Model
To interact with the OpenAI language model, you'll need to initialize it with your API key and desired settings. 
```javascript
// Instantiate Model
const model = new ChatOpenAI({
  modelName: "gpt-3.5-turbo",
  temperature: 0.7,
  openAIApiKey: process.env.OPENAI_API_KEY, 
});
```
The API Key is stored as an environmental variable. Proceed to the root folder of your project and create a file named **.env**. Store your OpenAI API key there as follows:
```bash
OPENAI_API_KEY=Your API Key

```
**Temperature** is a hyperparameter that controls the randomness of the model's output.
### Fetching FAQ Data From Strapi
The system relies on pre-defined FAQ data stored in Strapi. Define a function to fetch this data using Axios and make a `GET` request to the Strapi API endpoint you configured earlier.
```javascript
// Fetch FAQ data
const fetchData = async () => {
  try {
    const response = await axios.get("http://localhost:1337/api/faqs");
    return response.data;
  } catch (error) {
    console.error("Error fetching data:", error.message);
    return [];
  }
};
```
After fetching the data, extract the questions and their corresponding answers.
```javascript
const extractQuestionsAndAnswers = (data) => {
  return data.data.map((item) => {
    return {
      question: item.attributes.Question,
      answer: item.attributes.Answer[0].children[0].text,
    };
  });
};
```
The above function maps through the data array and extract the question and answer attributes from each item.
### Populating the Vector Store
To efficiently retrieve relevant answers, create a vector store containing embeddings of the FAQ documents. 
```javascript
// Populate Vector Store
const populateVectorStore = async () => {
  const data = await fetchData();
  const questionsAndAnswers = extractQuestionsAndAnswers(data);

  // Create documents from the FAQ data
  const docs = questionsAndAnswers.map(({ question, answer }) => {
    return new Document({ pageContent: `${question}\n${answer}`, metadata: { question } });
  });

  // Text Splitter
  const splitter = new RecursiveCharacterTextSplitter({ chunkSize: 100, chunkOverlap: 20 });
  const splitDocs = await splitter.splitDocuments(docs);

  // Instantiate Embeddings function
  const embeddings = new OpenAIEmbeddings();

  // Create the Vector Store
  const vectorstore = await MemoryVectorStore.fromDocuments(splitDocs, embeddings);
  return vectorstore;
};

```
The above code uses the questions and answers data to create document objects. It then splits them into smaller chunks, computes embeddings, and constructs a vector store. 

The vector store holds representations of the FAQ data, facilitating efficient retrieval and processing within the system.
### Answering Questions From the Vector Store
Having the vector store full of information, you need a way to retrieve only the relevant information to a user query. Then use an LLM to come up with a good response to the query based on the retrieved information and the chat history.

To achieve this, you will implement a function to create a retriever, define prompts for AI interaction, and invoke a retrieval chain.
```javascript
// Logic to answer from Vector Store
const answerFromVectorStore = async (chatHistory, input) => {
  const vectorstore = await populateVectorStore();

  // Create a retriever from vector store
  const retriever = vectorstore.asRetriever({ k: 4 });

  // Create a HistoryAwareRetriever which will be responsible for
  // generating a search query based on both the user input and
  // the chat history
  const retrieverPrompt = ChatPromptTemplate.fromMessages([
    new MessagesPlaceholder("chat_history"),
    ["user", "{input}"],
    [
      "user",
      "Given the above conversation, generate a search query to look up in order to get information relevant to the conversation",
    ],
  ]);

  // This chain will return a list of documents from the vector store
  const retrieverChain = await createHistoryAwareRetriever({
    llm: model,
    retriever,
    rephrasePrompt: retrieverPrompt,
  });

  // Define the prompt for the final chain
  const prompt = ChatPromptTemplate.fromMessages([
    [
      "system",
      `You are a Strapi CMS FAQs assistant. Your knowledge is limited to the information I  provide in the context. 
       You will answer this question based solely on this information: {context}. Do not make up your own answer .
       If the answer is not present in the information, you will respond 'I don't have that information.
       If a question is outside the context of Strapi, you will respond 'I can only help with Strapi related questions.`,
    ],
    new MessagesPlaceholder("chat_history"),
    ["user", "{input}"],
  ]);
  
  // the createStuffDocumentsChain
  const chain = await createStuffDocumentsChain({
    llm: model,
    prompt: prompt,
  });

  // Create the conversation chain, which will combine the retrieverChain
  // and combineStuffChain to get an answer
  const conversationChain = await createRetrievalChain({
    combineDocsChain: chain,
    retriever: retrieverChain,
  });

  // Get the response
  const response = await conversationChain.invoke({
    chat_history: chatHistory,
    input: input,
  });

  // Log the response to the server console
  console.log("Server response:", response);
  return response;
};

```
The above code creates a retriever for search queries and configures a history-aware retriever. It then defines prompts for AI interaction, constructs a conversation chain, and invokes it with chat history and input. Finally, it logs and returns the generated response.
### Handling Incoming Requests and Starting the Server
Now that you have everything for handling a user request ready, expose a `POST` endpoint `/chat` to handle incoming requests from clients. The route handler will parse input data, format the chat history, and pass it to the `answerFromVectorStore` function responsible for answering questions.
```javascript
// Route to handle incoming requests
app.post("/chat", async (req, res) => {
  const { chatHistory, input } = req.body;

  // Convert the chatHistory to an array of HumanMessage and AIMessage objects
  const formattedChatHistory = chatHistory.map((message) => {
    if (message.role === "user") {
      return new HumanMessage(message.content);
    } else {
      return new AIMessage(message.content);
    }
  });

  const response = await answerFromVectorStore(formattedChatHistory, input);
  res.json(response);
});

// Start the server
app.listen(PORT, () => {
  console.log(`Server is running on http://localhost:${PORT}`);
});

```
Run the following command on your terminal to start the server:
```javascript
node server.mjs

```
The server will run on the specified port.

Use **[Postman](https://www.postman.com/downloads/)** or any other software to test the server. Make sure the payload you send is in this format:

```javascript
{
    "chatHistory": [
        {
            "role": "user",
            "content": "What is Strapi?"
        },
        {
            "role": "assistant",
            "content": "Strapi is an open-source headless CMS (Content Management System) "
        }
    ],
    "input": "Does Strapi have a default limit"
}
```
You can change the content and input data to your liking. Below is a sample result after you make the post request:

```javascript

"answer": "The default limit for records in the Strapi API is 100."

```
That is the answer part of the response. But the response has a lot more data in it including the documents used to answer the question.

## Creating the Frontend of Your System
Having the core part of your system completed. You need a user interface in which the users will interact with your system. Under **src** in your React app, create a **ChatbotUI.js** file and paste the following code:
```javascript
import React, { useState, useEffect, useRef } from 'react';
import axios from 'axios';
import './ChatbotUI.css'; // Assuming the CSS file exists

const ChatbotUI = () => {
  const [chatHistory, setChatHistory] = useState([]);
  const [userInput, setUserInput] = useState('');
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);
  const [isExpanded, setIsExpanded] = useState(true); // State for chat window expansion
  const chatContainerRef = useRef(null);

  useEffect(() => {
    // Scroll to the bottom of the chat container when new messages are added
    if (isExpanded) {
      chatContainerRef.current.scrollTop = chatContainerRef.current.scrollHeight;
    }
  }, [chatHistory, isExpanded]);

  const handleUserInput = (e) => {
    setUserInput(e.target.value);
  };

  const handleSendMessage = async () => {
    if (userInput.trim() !== '') {
      const newMessage = { role: 'user', content: userInput };
      const updatedChatHistory = [...chatHistory, newMessage];
      setChatHistory(updatedChatHistory);
      setUserInput('');
      setIsLoading(true);

      try {
        const response = await axios.post('http://localhost:30080/chat', {
          chatHistory: updatedChatHistory,
          input: userInput,
        });
        const botMessage = {
          role: 'assistant',
          content: response.data.answer,
        };
        setChatHistory([...updatedChatHistory, botMessage]);
      } catch (error) {
        console.error('Error sending message:', error);
        setError('Error sending message. Please try again later.');
      } finally {
        setIsLoading(false);
      }
    }
  };

  const toggleChatWindow = () => {
    setIsExpanded(!isExpanded);
  };

  return (
    <div className="chatbot-container">
      <button className="toggle-button" onClick={toggleChatWindow}>
        {isExpanded ? 'Collapse Chat' : 'Expand Chat'}
      </button>
      {isExpanded && (
        <div className="chat-container" ref={chatContainerRef}>
          {chatHistory.map((message, index) => (
            <div
              key={index}
              className={`message-container ${
                message.role === 'user' ? 'user-message' : 'bot-message'
              }`}
            >
              <div
                className={`message-bubble ${
                  message.role === 'user' ? 'user-bubble' : 'bot-bubble'
                }`}
              >
                <div className="message-content">{message.content}</div>
              </div>
            </div>
          ))}
          {error && <div className="error-message">{error}</div>}
        </div>
      )}
      <div className="input-container">
        <input
          type="text"
          placeholder="Type your message..."
          value={userInput}
          onChange={handleUserInput}
          onKeyPress={(e) => {
            if (e.key === 'Enter') {
              handleSendMessage();
            }
          }}
          disabled={isLoading}
        />
        <button onClick={handleSendMessage} disabled={isLoading}>
          {isLoading ? 'Loading...' : 'Send'}
        </button>
      </div>
    </div>
  );
};

export default ChatbotUI;

```
The above code creates a user interface for interacting with the **AI-powered FAQ system** hosted on the server. It allows users to send messages, view chat history, and receive responses from the server. It also maintains a state for chat history, user input, loading status, and error handling. When a user sends a message, the component sends an HTTP `POST` request to the server's `/chat endpoint`, passing along the updated chat history and user input. Upon receiving a response from the server, it updates the chat history with the bot's message.

Create another file under `src` directory and name it `ChatbotUI.css` and paste the following code. This code will be responsible for styling the user interface.
```css
.chatbot-container {
    display: flex;
    flex-direction: column;
    background-color: #f5f5f5; 
    padding: 5px; 
    position: fixed; 
    bottom: 10px;  
    right: 10px;  
    width: 300px;  
    z-index: 10;  
  }
  
  .toggle-button {
    padding: 5px 10px;
    background-color: #ddd; 
    border: 1px solid #ccc;
    border-radius: 5px;
    cursor: pointer;
    margin-bottom: 5px; 
  }
  
  .chat-container {
    height: 300px;
    overflow-y: auto;
  }
  
  .message-container {
    display: flex;
    justify-content: flex-start;
    margin-bottom: 5px; /* Reduced margin for tighter spacing */
  }
  
  .message-bubble {
    max-width: 70%;
    padding: 5px; /* Reduced padding for smaller bubbles */
    border-radius: 10px;
  }
  
  .user-bubble {
    background-color: #007bff;
    color: white;
  }
  
  .bot-bubble {
    background-color: #f0f0f0;
    color: black;
  }
  
  .input-container {
    align-self: flex-end;
    display: flex;
    align-items: center;
    padding: 5px; 
  }
  
  .input-container input {
    flex: 1;
    padding: 5px; 
    border: 1px solid #ccc;
    border-radius: 5px;
    margin-right: 10px;
  }
  
  .input-container button {
    padding: 10px 20px;
    background-color: #007bff;
    color: white;
    border: none;
    border-radius: 5px;
    cursor: pointer;
  }
  
```
The above code defines the layout and styling for the user interface. It positions the chat interface fixed at the bottom right corner of the screen, styles message bubbles, and formats the input field and send button for user interaction.

In the `App.js` file render the user interface.
```javascript
import React from 'react';
import ChatbotUI from './ChatbotUI';

const App = () => {
  return (
    <div>
      <ChatbotUI />
    </div>
  );
};

export default App;
```

You are now done creating the FAQ AI-powered system.

Open a new terminal in the same path you run your server and start your react app using the following command:
```bash
yarn start
```
You can now start asking the system FAQs about Strapi CMS. The system knowledge depends on the FAQ data you have stored in Strapi.

## Testing the System
The following GIF shows how the system responds:

![Results](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7yp6vq3m9ptku53kg726.gif)
When asked about a topic outside Strapi, it reminds the user it only deals with Strapi CMS. Also if an answer is not present in the FAQ data stored in Strapi CMS, it responds it does not have that information.

## Conclusion
Congratulations on creating an AI & Strapi-powered FAQ system. In this tutorial, you've learned how to leverage the strengths of Strapi, LangChain.js, and OpenAI.

The system integrates seamlessly with Strapi, allowing you to effortlessly manage your FAQ data through a centralized platform. LangChain.js facilitates Retrieval Augmented Generation (RAG), enhancing the accuracy and comprehensiveness of the system's responses. OpenAI provides the large language model that the system uses to generate informative and relevant answers to user queries. 

## Resources
* Have a look at the full code [here](https://github.com/FINCH285/AI-Powered-FAQ-System-with-Strapi-LangChain-OpenAI) and the Strapi backend [here](https://github.com/FINCH285/faq-bot-strapi-backend).
* https://js.langchain.com/docs/get_started/introduction
* https://docs.strapi.io/dev-docs/backend-customization
* https://blogs.nvidia.com/blog/what-is-retrieval-augmented-generation/