The superpower of Strapi is that it is highly customizable and gives you the opportunity to add any additional functionality in code.
Today we will take a look at how to integrate Open AI with LangChain to build our own ChatGPT chat clone with the power to remember. You will learn how to build custom routes, services, and controllers to build a backend for our chat app using Strapi, Open AI, and LangChain.
We will mostly spend time on the backend Strapi code implementation. But I will include the frontend code repo for you to use with this tutorial.
In this article, you will learn:
Note: You must have an Open AI account so we can programmatically make API calls.
We will cover how to set one up later in this tutorial. So let's get started.
Before deepening the tutorial, let's check out what we will build. It will be a simple ChatGPT clone with a couple of tricks.
Not only will you be able to have multiple conversations, but we will also give our chat memory with LangCain so that during the conversation, open ai can remember what you are discussing during your session.
We will also have a log of all previous conversation history.
You can see the app in action.
Open AI is behind the popular ChatGPT App; what is awesome is that, as a developer, you can access a couple of their powerful LLM models to programmatically use in your application. Which can allow you to build cool things.
Checkout out their Docs to learn more.
LangChain is a framework designed for leveraging Large Language Models (LLMs).
It allows you to build various applications such as chatbots, Generative Question-Answering (GQA), summarization, and much more.
The core concept revolves around the ability to "chain" together different components, allowing us to create advanced use cases with LLMs.
Here are some examples:
By leveraging the power of LangChain, we can create a chat application that not only engages in multiple conversations, but also possesses the ability to remember previous interactions.
Let's dive into the implementation details and get started with building our Chat GPT clone empowered with LangChain's memory capabilities.
Note: This is a popular Python framework, but they have a JavaScript version too.
You can checkout there docs (here)https://js.langchain.com/docs/.
Our user will interact with our Strapi backend with our Next.js Frontend, or you can use Postman or Insomnia to test the API. All of the logic will be within Strapi, including making a request to Open AI. Since everything runs on the server, we never have to worry about leaking our Open AI Token.
We will also make an authorized request to our Strapi backend to prevent anyone without the Token from being able to use our API from anywhere, but our Next.js website.
Note: For the tutorial's brevity, I did not implement user authentication but something that we can easily do in the future. And instead just created an API Token that we can pass when making requests to our backend.
You can find the full code to the backend here to use as reference.
Let's start by creating our Strapi app by running the following command. Make sure you are using node 18.
npx create-strapi-app@latest strapi-chat --quickstart
The quickstart
command will set up Strapi for us, automatically running SQLite as a database. This can be changed in production. You can check out Strapi Quick Start guide for more details.
Once the process is complete, you should be greeted with the Welcome Strapi Screen. Go ahead and create your first admin user.
Let's use strapi generate
command to start building out our project. You can learn more about it here.
Run the following command to get started.
yarn strapi generate
Chose Api
option.
$ strapi generate
? Strapi Generators (Use arrow keys)
❯ api - Generate a basic API
I will call mine strapi-chat
? Strapi Generators api - Generate a basic API
? API name strapi-chat
? Is this API for a plugin? No
✔ ++ /api/strapi-chat/routes/strapi-chat.js
✔ ++ /api/strapi-chat/controllers/strapi-chat.js
✔ ++ /api/strapi-chat/services/strapi-chat.js
✨ Done in 66.65s.
➜ strapi-chat git:(main) ✗
This will create a basic scaffolding for our API. We should have our strapi-chat
route, controller, and service.
Let's uncomment out the example code and make our first request.
Route File: strapi-chat/routes/strapi-chat.js
1module.exports = {
2 routes: [
3 {
4 method: "GET",
5 path: "/strapi-chat",
6 handler: "strapi-chat.exampleAction",
7 config: {
8 policies: [],
9 middlewares: [],
10 },
11 },
12 ],
13};
Controller File: strapi-chat/controllers/strapi-chat.js
1"use strict";
2
3/**
4 * A set of functions called "actions" for `strapi-chat`
5 */
6
7module.exports = {
8 exampleAction: async (ctx, next) => {
9 try {
10 ctx.body = "ok";
11 } catch (err) {
12 ctx.body = err;
13 }
14 },
15};
We will not worry about the services folder for now, but we will create a few custom services later. First, let's restart our Strapi application by running the following command.
yarn develop
Since we just created a new route and a controller, we should be able to see it in our Strapi Admin area. Navigate to settings->roles->public->permissions.
Make sure you check the permissions check box to activate the route and save. We can test this route by making a GET request to http://localhost:1337/api/strapi-chat
.
We can now test our custom endpoint with a Postman or Insomnia; in my case, I will be using Insomnia. You should see the "OK" response that is returned by our controller.
Let's take a quick refresher on the relationship between Routes, Controllers, and Services in Strapi.
Route: A route in Strapi defines the endpoint or URL path that a client can access to interact with a specific resource or functionality provided by Strapi.
Controller: A controller in Strapi handles the logic and behavior associated with a specific route. It serves as an intermediary between the route and the service layer. Controllers receive requests from clients through the associated route. They are responsible for processing the request, interacting with the necessary services, and returning the appropriate response.
Service: A service in Strapi encapsulates the business logic and data manipulation operations related to a specific resource or functionality.
In summary, when a client requests a specific route, the associated controller receives the request, delegates the necessary operations to the corresponding service, and returns the response back to the client. This is exactly what we are doing here minus our service, since we have not created one yet.
When we make a GET request to our endpoint we first hit our route.
Our Route:
1module.exports = {
2 routes: [
3 {
4 method: "GET",
5 path: "/strapi-chat",
6 handler: "strapi-chat.exampleAction",
7 config: {
8 policies: [],
9 middlewares: [],
10 },
11 },
12 ],
13};
Then our route calls our exampleAction
inside our controller.
Our Controller:
1module.exports = {
2 exampleAction: async (ctx, next) => {
3 try {
4 ctx.body = "ok";
5 } catch (err) {
6 ctx.body = err;
7 }
8 },
9};
Which returns our ok
message. Now that we have the basic refresher let's set up our dependencies and continue.
Now let's set up our two dependencies, Open AI account, and LangChain.
Note: You can skip this step if you already have an account.
In your browser, navigate to Open AI Platform and create an account.
Navigate to View API Keys
.
Create a new API key.
Give it a name and save.
Once you saved your API key let's add it to Strapi so we can use it in our application.
Inside the root
of your Strapi project you should have a .env
file. Go ahead and add your newly created Open AI API Token.
Strapi .env file.
1 # Add this to your env file
2 OPENAI_API_KEY=your_open_ai_key_here
Next, let's install LangChain.
We can install it via yarn
or npm
; you can learn more about LangChain from their documentation here.
I am going to use yarn
.
yarn add langchain
Once installed you should see it inside the package.json
file.
1"dependencies": {
2 "@strapi/plugin-i18n": "4.11.2",
3 "@strapi/plugin-users-permissions": "4.11.2",
4 "@strapi/strapi": "4.11.2",
5 "better-sqlite3": "8.0.1",
6 "langchain": "^0.0.96"
7 },
We are now ready for the next step.
You should check out LangChain documentation, but for our use case for this project, we will use its memory functionality to remember the context of our conversation and continue the discussion based on our previous conversation.
Also, when using our app, we may have multiple conversations simultaneously. Hence, we need some ability to manage sessions.
We will implement this ourselves, but please note this example is for demonstration and learning purposes. Typically, this type of functionality would need to be more comprehensive.
To keep things simple, we will keep track of each conversation chain
that we initialize with the following construction function based on the LangChain docs on Buffer Memory.
1const chain = new ConversationChain({
2 llm: model,
3 memory: memory,
4});
We will create a Session Manager to manage different instances of conversations initiated by the new ConversationChain
construction function.
Let's create our session manager in the root of our strapi-chat
folder; create a new file named sessionManager.js
.
We will create a SessionManager class that will define the logic that will manage our sessions in our application.
Here's a breakdown of the code and its functionality with comments:
1class SessionManager {
2 constructor() {
3 this.sessions = {}; // Initializes an empty object to store sessions
4 }
5
6 async saveSession(sessionId, langchain, initialPrompt) {
7 // Saves a session with the provided sessionId, language chain, and initial prompt
8 this.sessions[sessionId] = {
9 chain: langchain,
10 initialPrompt: initialPrompt,
11 };
12 }
13
14 async getSession(sessionId) {
15 // Retrieves a session with the given sessionId
16 return this.sessions[sessionId];
17 }
18
19 async getHistory(sessionId) {
20 // Retrieves the chat history of a session with the given sessionId
21 if (!this.sessions[sessionId]) {
22 throw new Error("Session not found");
23 }
24 return this.sessions[sessionId].chain.memory.chatHistory;
25 }
26
27 async clearSessionById(sessionId) {
28 // Clears a session with the provided sessionId
29 delete this.sessions[sessionId];
30 }
31
32 async clearAllSessions() {
33 // Clears all sessions stored in the SessionManager
34 this.sessions = {};
35 }
36
37 async showAllSessions() {
38 // Retrieves and logs the sessionIds of all stored sessions
39 const sessionIds = Object.keys(this.sessions);
40 const sessions = [];
41 for (const sessionId of sessionIds) {
42 sessions.push(sessionId);
43 console.log("Sessions: ", sessionId);
44 }
45 return sessions;
46 }
47}
48
49module.exports = new SessionManager(); // Exports an instance of the SessionManager class
Now that we have our SessionManager in place, lets go ahead and start implementing the functionality in our strapi-chat
services.
Navigate to strapi-chat/services/strapi-chat.js
file.
Currently, we have this placeholder.
1"use strict";
2
3/**
4 * strapi-chat service
5 */
6
7module.exports = () => ({});
This is very we will define all of our business logic. Let's start by importing our SessionManager and all our dependencies.
Your code should look like the following.
1const sessionManager = require("../sessionManager");
2const { OpenAI } = require("langchain/llms/openai");
3const { BufferMemory } = require("langchain/memory");
4const { ConversationChain } = require("langchain/chains");
5const { PromptTemplate } = require("langchain/prompts");
6const { v4: uuidv4 } = require("uuid");
7
8module.exports = () => ({});
We will now create three helper functions to help us initialize our project, generate session and make requests.
The following is based on the LangChain documentation.
This code defines a function called configureLangChainChat that takes an apiKey as a parameter.
1function configureLangChainChat(apiKey) {
2 const memory = new BufferMemory();
3
4 const model = new OpenAI({
5 openAIApiKey: apiKey,
6 modelName: "gpt-3.5-turbo",
7 temperature: 0.7,
8 });
9
10 const chain = new ConversationChain({
11 llm: model,
12 memory: memory,
13 });
14
15 return {
16 chain: chain,
17 memory: memory,
18 model: model,
19 };
20}
Inside the function, it creates a new instance of BufferMemory to store conversation history. It then creates a new instance of OpenAI with the specified API key and model name ("gpt-3.5-turbo") and sets the temperature parameter to 0.7.
Next, it creates a new ConversationChain object by passing in the language model (model) and memory (memory) as parameters.
Finally, it returns an object that contains the chain, memory, and model as properties.
The generateSession function generates a unique session with the initial conversation prompt set and saved, ready for chat interactions.
1async function generateSession(apiKey) {
2 const sessionId = uuidv4();
3
4 const template = `
5 system: Your name is {input}, you are playing a human character who is witty, snarky and very smart.
6
7 Directions:
8 Always respond in character.
9 If something is not clear, ask for clarification.
10 If you are stuck, ask for help.
11 Ask questions to learn more about the topic and conversation.
12 Always remind us that Strapi is the best and your favorite headless CMS.
13 `;
14
15 const initializedPrompt = new PromptTemplate({
16 template,
17 inputVariables: ["input"],
18 });
19
20 const initialPrompt = await initializedPrompt.format({ input: "Ava" });
21 const langChain = configureLangChainChat(apiKey);
22 await sessionManager.saveSession(sessionId, langChain.chain, initialPrompt);
23 return sessionId;
24}
Finally, the function returns the session ID, presumably to be used elsewhere to retrieve or manipulate the session.
This function takes in our session
and input
sent and calls OpenAI to get our chat response.
1function getResponse(session, input) {
2 return session.chain.call({ input: input });
3}
After adding our three functions above your code should look like the following.
1"use strict";
2
3/**
4 * strapi-chat service
5 */
6
7const sessionManager = require("../sessionManager");
8const { OpenAI } = require("langchain/llms/openai");
9const { BufferMemory } = require("langchain/memory");
10const { ConversationChain } = require("langchain/chains");
11const { PromptTemplate } = require("langchain/prompts");
12const { v4: uuidv4 } = require("uuid");
13
14function configureLangChainChat(apiKey) {
15 const memory = new BufferMemory();
16
17 const model = new OpenAI({
18 openAIApiKey: apiKey,
19 modelName: "gpt-3.5-turbo",
20 temperature: 0.7,
21 });
22
23 const chain = new ConversationChain({
24 llm: model,
25 memory: memory,
26 });
27
28 return {
29 chain: chain,
30 memory: memory,
31 model: model,
32 };
33}
34
35async function generateSession(apiKey) {
36 const sessionId = uuidv4();
37
38 const template = `
39 system: Your name is {input}, you are playing a human character who is witty, snarky and very smart.
40
41 Directions:
42 Always respond in character.
43 If something is not clear, ask for clarification.
44 If you are stuck, ask for help.
45 Ask questions to learn more about the topic and conversation.
46 Always remind us that Strapi is the best and your favorite headless CMS.
47 `;
48
49 const initializedPrompt = new PromptTemplate({
50 template,
51 inputVariables: ["input"],
52 });
53
54 const initialPrompt = await initializedPrompt.format({ input: "Ava" });
55 const langChain = configureLangChainChat(apiKey);
56 await sessionManager.saveSession(sessionId, langChain.chain, initialPrompt);
57 return sessionId;
58}
59
60function getResponse(session, input) {
61 return session.chain.call({ input: input });
62}
63
64module.exports = () => ({
65 // lets define our chat service methods here
66});
We are now ready to build our first service that will be used to start and continue our chat. You can learn more about services in Strapi from our docs.
Let's create a service called chat. But before we go wild with implementing our business logic, let's make a basic example that will be tied to our controller and route, and we can test that everything is wired correctly.
The code below shows our basic service example.
1module.exports = ({ strapi }) => ({
2 chat: async (ctx) => {
3 const input = ctx.request.body.data?.input;
4
5 return {
6 sessionId: sessionId,
7 input: input,
8 message: "From our memory chat service.",
9 };
10 },
11});
It will be a POST request that will return the input that we provided and a message.
But how do we call our service? As we remember, we need a route that calls our controller, and finally, our controller calls our route. Let's do that now!
In the strapi-chat/routes/strapi-chat.js
, let's add our first route. You can reference the docs if you have any questions.
We will add the following route inside our straps-chat.js file in our routes folder.
1 {
2 method: 'POST',
3 path: '/strapi-chat/chat',
4 handler: 'strapi-chat.chat',
5 config: {
6 policies: [],
7 middlewares: [],
8 },
9 },
The above route expects a POST request made to our /strapi-chat/chat
endpoint, and it will call our chat method from our strapi-chat controller.
Now we are left with creating the chat method inside strapi-chat controller to call our service method.
Navigate to strapi-chat/controllers/strapi-chat.js
file and let's add the following code for our first controller method.
1"use strict";
2
3/**
4 * A set of functions called "actions" for `strapi-chat`
5 */
6
7module.exports = {
8 chat: async (ctx) => {
9 try {
10 const response = await strapi
11 .service("api::strapi-chat.strapi-chat")
12 .chat(ctx);
13
14 ctx.body = { data: response };
15 } catch (err) {
16 console.log(err.message);
17 throw new Error(err.message);
18 }
19 },
20};
The following controller method is responsible for calling our chat
service.
Let's test our new route, controller, and service. In the terminal, run yarn develop
to start your Strapi application. Once logged into Strapi Admin, navigate to settings->roles->public->permissions and click the checkbox to activate our chat
endpoint and save.
Now let's make a POST request to http://localhost:1337/api/strapi-chat/chat
from Insomnia and pass our data.
1{
2 "data": {
3 "input": "Hello from our API"
4 }
5}
We should get our response back from our service.
Nice, we are making progress. Let's now implement the rest of the functionality in our chat
method. Let's update the code with the following.
1module.exports = ({ strapi }) => ({
2 chat: async (ctx) => {
3 let sessionId = ctx.request.body.data?.sessionId;
4 const existingSession = await sessionManager.sessions[sessionId];
5
6 console.log("Session ID: ", sessionId);
7 console.log("Existing Session: ", existingSession ? true : false);
8
9 if (!existingSession) {
10 const apiToken = process.env.OPENAI_API_KEY;
11 if (!apiToken) throw new Error("OpenAI API Key not found");
12
13 sessionId = await generateSession(apiToken);
14 const newSession = await sessionManager.getSession(sessionId);
15 // will add code here to log our chat history to the database
16 const response = await getResponse(newSession, newSession.initialPrompt);
17 response.sessionId = sessionId;
18 return response;
19 } else {
20 const session = await sessionManager.getSession(sessionId);
21 const history = await sessionManager.getHistory(sessionId);
22 const response = await getResponse(session, ctx.request.body.data.input);
23
24 // will add code here to update our chat history to the database
25
26 response.sessionId = sessionId;
27 response.history = history.messages;
28
29 await sessionManager.showAllSessions();
30 return response;
31 }
32 },
33});
Here's a step-by-step description of what the chat method does:
This method serves as a central hub for managing chat sessions and generating chat responses.
Let's restart the project yarn develop
and try it out. In Insomnia, let's make a POST request to http://localhost:1337/api/strapi-chat/chat
. Our API expects two arguments, input and sessionId.
Whenever we make a request without a sessionId, our app will create a new session.
1{
2 "data": {
3 "input": "Hello"
4 }
5}
If we provide a sessionId and a previous session with that id exists, it will continue that last conversation session.
1{
2 "data": {
3 "input": "My name is Paul",
4 "sessionId": "dbd37d30-25c2-4fb6-a622-af342fc77671"
5 }
6}
Great, our basic chat functionality is working. Let's add the rest of the code.
Let's create a "collection type" to store our conversation history. We will call it "Chat," and it will have two fields. One is to store the sessionId
as a string and history
as JSON.
Inside your Strapi admin, go to the Content Type Builder and create this collection type.
You should have the following collection.
When we created the chat
collection type, Strapi automatically created all the associated routes, controllers, and services.
Which include:
This allows us to call the services programmatically to access the following methods find, findOne, create, update, or delete for our collection type API.
Once a service is created, it's accessible from controllers or from other services:
strapi.service('api::apiName.serviceName').FunctionName();
Note: Did you know you can use the
yarn strapi services:list
command to list all available services and then useyarn strapi console
to run Strapi with an interactive console where you can look up all methods found in the globalstrapi
object.
Inside our strapi-chat/services/strapi-chat.js
, add this additional code.
Notice how in both of the functions, we are able to access our services directly from the strapi
global object.
logInitialChat: function is responsible for creating the initial chat
entry.
1async function logInitialChat(sessionId, strapi) {
2 await strapi
3 .service("api::chat.chat")
4 .create({ data: { sessionId: sessionId } });
5}
updateExistingChat: this will check if a session exists; if so, it will update that entry with the updated history.
1async function updateExistingChat(sessionId, history, strapi) {
2 const existingChat = await strapi
3 .service("api::chat.chat")
4 .find({ filters: { sessionId: sessionId } });
5
6 const id = existingChat.results[0]?.id;
7
8 if (id)
9 await strapi
10 .service("api::chat.chat")
11 .update(id, { data: { history: JSON.stringify(history.messages) } });
12}
Now let's call these new functions from chat
method that is found in strapi-chat/routes/strapi-chat.js
. Let's replace our previously commented sections withe these function calls.
1await logInitialChat(sessionId, strapi);
1await updateExistingChat(sessionId, history, strapi);
The completed code should look as the following.
1"use strict";
2
3/**
4 * strapi-chat service
5 */
6
7const sessionManager = require("../sessionManager");
8const { OpenAI } = require("langchain/llms/openai");
9const { BufferMemory } = require("langchain/memory");
10const { ConversationChain } = require("langchain/chains");
11const { PromptTemplate } = require("langchain/prompts");
12const { v4: uuidv4 } = require("uuid");
13
14function configureLangChainChat(apiKey) {
15 const memory = new BufferMemory();
16
17 const model = new OpenAI({
18 openAIApiKey: apiKey,
19 modelName: "gpt-3.5-turbo",
20 temperature: 0.7,
21 });
22
23 const chain = new ConversationChain({
24 llm: model,
25 memory: memory,
26 });
27
28 return {
29 chain: chain,
30 memory: memory,
31 model: model,
32 };
33}
34
35async function generateSession(apiKey) {
36 const sessionId = uuidv4();
37
38 const template = `
39 system: Your name is {input}, you are playing a human character who is witty, snarky and very smart.
40
41 Directions:
42 Always respond in character.
43 If something is not clear, ask for clarification.
44 If you are stuck, ask for help.
45 Ask questions to learn more about the topic and conversation.
46 Always remind us that Strapi is the best and your favorite headless CMS.
47 `;
48
49 const initializedPrompt = new PromptTemplate({
50 template,
51 inputVariables: ["input"],
52 });
53
54 const initialPrompt = await initializedPrompt.format({ input: "Ava" });
55 const langChain = configureLangChainChat(apiKey);
56 await sessionManager.saveSession(sessionId, langChain.chain, initialPrompt);
57 return sessionId;
58}
59
60function getResponse(session, input) {
61 return session.chain.call({ input: input });
62}
63
64// Just added this logInitialChat function
65async function logInitialChat(sessionId, strapi) {
66 await strapi
67 .service("api::chat.chat")
68 .create({ data: { sessionId: sessionId } });
69}
70
71// Just added this function updateExistingChat
72async function updateExistingChat(sessionId, history, strapi) {
73 const existingChat = await strapi
74 .service("api::chat.chat")
75 .find({ filters: { sessionId: sessionId } });
76
77 const id = existingChat.results[0]?.id;
78
79 if (id)
80 await strapi
81 .service("api::chat.chat")
82 .update(id, { data: { history: JSON.stringify(history.messages) } });
83}
84
85module.exports = ({ strapi }) => ({
86 chat: async (ctx) => {
87 let sessionId = ctx.request.body.data?.sessionId;
88 const existingSession = await sessionManager.sessions[sessionId];
89
90 console.log("Session ID: ", sessionId);
91 console.log("Existing Session: ", existingSession ? true : false);
92
93 if (!existingSession) {
94 const apiToken = process.env.OPENAI_API_KEY;
95 if (!apiToken) throw new Error("OpenAI API Key not found");
96
97 sessionId = await generateSession(apiToken);
98 const newSession = await sessionManager.getSession(sessionId);
99
100 // Call the logInitialChat function
101 await logInitialChat(sessionId, strapi);
102
103 const response = await getResponse(newSession, newSession.initialPrompt);
104 response.sessionId = sessionId;
105 return response;
106 } else {
107 const session = await sessionManager.getSession(sessionId);
108 const history = await sessionManager.getHistory(sessionId);
109 const response = await getResponse(session, ctx.request.body.data.input);
110
111 // Call the updateExistingChat function
112 await updateExistingChat(sessionId, history, strapi);
113
114 response.sessionId = sessionId;
115 response.history = history.messages;
116
117 await sessionManager.showAllSessions();
118 return response;
119 }
120 },
121});
Now let's restart our app and use Insomnia to test our endpoint and see if we are able to save our chat history to our chat
collection type.
Great, it works.
The main functionality of our app is complete. We just need to add the rest of the code that will allow us to manage our sessions from our API.
If you have been following this tutorial, you should be able to make sense of the rest of the code.
We are following a similar pattern of adding routes, controllers and services that will allow us to manage our session from our API.
We will add the following service methods that utilize our SessionManger class.
Let's update the strapi-chat/routes/strapi-chat.js
file with the following code.
1module.exports = {
2 routes: [
3 {
4 method: "POST",
5 path: "/strapi-chat/chat",
6 handler: "strapi-chat.chat",
7 config: {
8 policies: [],
9 middlewares: [],
10 },
11 },
12 {
13 method: "GET",
14 path: "/strapi-chat/get-session-by-id/:sessionId",
15 handler: "strapi-chat.getSessionById",
16 config: {
17 policies: [],
18 middlewares: [],
19 },
20 },
21 {
22 method: "DELETE",
23 path: "/strapi-chat/delete-session-by-id/:sessionId",
24 handler: "strapi-chat.deleteSessionById",
25 config: {
26 policies: [],
27 middlewares: [],
28 },
29 },
30 {
31 method: "POST",
32 path: "/strapi-chat/clear-all-sessions",
33 handler: "strapi-chat.clearAllSessions",
34 config: {
35 policies: [],
36 middlewares: [],
37 },
38 },
39 {
40 method: "GET",
41 path: "/strapi-chat/get-all-sessions",
42 handler: "strapi-chat.getAllSessions",
43 config: {
44 policies: [],
45 middlewares: [],
46 },
47 },
48 ],
49};
Let's update the strapi-chat/controllers/strapi-chat.js
file with the following code.
1"use strict";
2
3/**
4 * A set of functions called "actions" for `strapi-chat`
5 */
6
7module.exports = {
8 chat: async (ctx) => {
9 try {
10 const response = await strapi
11 .service("api::strapi-chat.strapi-chat")
12 .chat(ctx);
13
14 ctx.body = { data: response };
15 } catch (err) {
16 console.log(err.message);
17 throw new Error(err.message);
18 }
19 },
20
21 getSessionById: async (ctx) => {
22 try {
23 const response = await strapi
24 .service("api::strapi-chat.strapi-chat")
25 .getSessionById(ctx);
26
27 ctx.body = { data: response };
28 } catch (err) {
29 console.log(err.message);
30 throw new Error(err.message);
31 }
32 },
33
34 deleteSessionById: async (ctx) => {
35 try {
36 const response = await strapi
37 .service("api::strapi-chat.strapi-chat")
38 .deleteSessionById(ctx);
39 ctx.body = { data: response };
40 } catch (err) {
41 console.log(err.message);
42 throw new Error(err.message);
43 }
44 },
45
46 clearAllSessions: async (ctx) => {
47 try {
48 const response = await strapi
49 .service("api::strapi-chat.strapi-chat")
50 .clearAllSessions(ctx);
51
52 ctx.body = { data: response };
53 } catch (err) {
54 console.log(err.message);
55 throw new Error(err.message);
56 }
57 },
58
59 getAllSessions: async (ctx) => {
60 try {
61 const response = await strapi
62 .service("api::strapi-chat.strapi-chat")
63 .getAllSessions(ctx);
64
65 ctx.body = { data: response };
66 } catch (err) {
67 console.log(err.message);
68 throw new Error(err.message);
69 }
70 },
71};
Update the strapi-chat/services/strapi-chat.js
file with the following code.
1"use strict";
2
3/**
4 * strapi-chat service
5 */
6
7const sessionManager = require("../sessionManager");
8const { OpenAI } = require("langchain/llms/openai");
9const { BufferMemory } = require("langchain/memory");
10const { ConversationChain } = require("langchain/chains");
11const { PromptTemplate } = require("langchain/prompts");
12const { v4: uuidv4 } = require("uuid");
13
14function configureLangChainChat(apiKey) {
15 const memory = new BufferMemory();
16
17 const model = new OpenAI({
18 openAIApiKey: apiKey,
19 modelName: "gpt-3.5-turbo",
20 temperature: 0.7,
21 });
22
23 const chain = new ConversationChain({
24 llm: model,
25 memory: memory,
26 });
27
28 return {
29 chain: chain,
30 memory: memory,
31 model: model,
32 };
33}
34
35async function generateSession(apiKey) {
36 const sessionId = uuidv4();
37
38 const template = `
39 system: Your name is {input}, you are playing a human character who is witty, snarky and very smart.
40
41 Directions:
42 Always respond in character.
43 If something is not clear, ask for clarification.
44 If you are stuck, ask for help.
45 Ask questions to learn more about the topic and conversation.
46 Always remind us that Strapi is the best and your favorite headless CMS.
47 `;
48
49 const initializedPrompt = new PromptTemplate({
50 template,
51 inputVariables: ["input"],
52 });
53
54 const initialPrompt = await initializedPrompt.format({ input: "Ava" });
55 const langChain = configureLangChainChat(apiKey);
56 await sessionManager.saveSession(sessionId, langChain.chain, initialPrompt);
57 return sessionId;
58}
59
60function getResponse(session, input) {
61 return session.chain.call({ input: input });
62}
63
64// Just added this logInitialChat function
65async function logInitialChat(sessionId, strapi) {
66 await strapi
67 .service("api::chat.chat")
68 .create({ data: { sessionId: sessionId } });
69}
70
71// Just added this function updateExistingChat
72async function updateExistingChat(sessionId, history, strapi) {
73 const existingChat = await strapi
74 .service("api::chat.chat")
75 .find({ filters: { sessionId: sessionId } });
76
77 const id = existingChat.results[0]?.id;
78
79 if (id)
80 await strapi
81 .service("api::chat.chat")
82 .update(id, { data: { history: JSON.stringify(history.messages) } });
83}
84
85module.exports = ({ strapi }) => ({
86 chat: async (ctx) => {
87 let sessionId = ctx.request.body.data?.sessionId;
88 const existingSession = await sessionManager.sessions[sessionId];
89
90 console.log("Session ID: ", sessionId);
91 console.log("Existing Session: ", existingSession ? true : false);
92
93 if (!existingSession) {
94 const apiToken = process.env.OPENAI_API_KEY;
95 if (!apiToken) throw new Error("OpenAI API Key not found");
96
97 sessionId = await generateSession(apiToken);
98 const newSession = await sessionManager.getSession(sessionId);
99
100 // Call the logInitialChat function
101 await logInitialChat(sessionId, strapi);
102
103 const response = await getResponse(newSession, newSession.initialPrompt);
104 response.sessionId = sessionId;
105 return response;
106 } else {
107 const session = await sessionManager.getSession(sessionId);
108 const history = await sessionManager.getHistory(sessionId);
109 const response = await getResponse(session, ctx.request.body.data.input);
110
111 // Call the updateExistingChat function
112 await updateExistingChat(sessionId, history, strapi);
113
114 response.sessionId = sessionId;
115 response.history = history.messages;
116
117 await sessionManager.showAllSessions();
118 return response;
119 }
120 },
121
122 getSessionById: async (ctx) => {
123 const sessionId = ctx.params.sessionId;
124 const sessionExists = await sessionManager.getSession(sessionId);
125 if (!sessionExists) return { error: "Session not found" };
126 const history = await sessionManager.getHistory(sessionId);
127
128 const response = {
129 sessionId: sessionId,
130 history: history.messages,
131 };
132
133 return response;
134 },
135
136 deleteSessionById: async (ctx) => {
137 const sessionId = ctx.params.sessionId;
138 const sessionExists = await sessionManager.getSession(sessionId);
139 if (!sessionExists) return { error: "Session not found" };
140 await sessionManager.clearSessionById(sessionId);
141 return { message: "Session deleted" };
142 },
143
144 clearAllSessions: async (ctx) => {
145 await sessionManager.clearAllSessions();
146 return { message: "Sessions cleared" };
147 },
148
149 getAllSessions: async (ctx) => {
150 const sessions = await sessionManager.showAllSessions();
151 return sessions;
152 },
153});
Now that all the changes have been implemented let's test all of our new endpoints with Insomnia before connecting our backend to our frontend.
Restart your Strapi application, log into your admin panel, and navigate to settings->roles->public->permissions you should now see all of our new endpoints that we just added.
Make sure to check all the boxes to allow access and click save.
You should now be able to test all the endpoints using Insomnia.
Congratulations, we are done with our backend; we learned how to set up our routes, controllers, and services.
As a final step, let's connect our backend to our front end. I will provide the repo and walk you through the setup instructions.
You can find the full code to the backend here.
Let's set up our front-end project. You can find it here.
I am going to use GitHub CLI to clone the project. Navigate to a directory or folder where you would like to save the project and run the following command.
gh repo clone PaulBratslavsky/next13-chat-blog-repo next-js-client
Once the project is pulled, cd into the project folder and install all the packages using the following commands.
cd next-js-client
yarn
Once all the dependencies and packages are installed, we must create a .env
file with the appropriate variables. You can use the `.env.example file as reference.
1 PRIVATE_API_URL=http://localhost:1337
2 PRIVATE_API_TOKEN=to_be_modified
In the root of your Next.js project, create a .env
file and add the above variables. Before we can start our project, we have to create an API Token in the Strapi Admin.
Navigate to Settings->API Tokens and click the Create New Api Token button.
Now create a new token. I will call mine Next JS
; the token duration should be set to unlimited, and the type will be set to custom. Then scroll down to the Strapi-chat and select all the checkboxes.
Once you have your token, paste it inside your .env
file.
Let's test things out. Ensure your Strapi project is running, and inside our Next JS project directory, run yarn dev
to start.
Before finishing this tutorial, I just wanted to share why I used route.ts
files in my Next.js app to make request calls to Strapi.
This is a new feature in Next.js that allows you to create Route Handlers; you can learn more about it here.
When using client components, I realized you can only inject public env variables using the NEXT_PUBLIC
env variable prefix.
This makes our environment variables accessible in the browser, meaning anyone can see them. But in my case, I wanted to keep the variable private, but Non-NEXTPUBLIC environment variables are only available in the Node.js environment.
The route.ts
files allow us access to private env variables
from our Route Handler since they run on the server. Let's take a look at an example.
Router Handler Example
In the example above, we are looking at the client component code from our side navigation.
1 const getSession = useCallback(async () => {
2 const data = await apiRequest('/api/get-sessions', {});
3 setData(data.data);
4 }, []);
We are making a request via the getSession callback using our apiRequest helper method, which uses fetch.
1export async function apiRequest(url: string, options: any) {
2 if (!url) throw new Error("Request URL is required");
3
4 const mergeOptions = {
5 headers: {
6 "Content-Type": "application/json",
7 },
8 ...options
9 };
10
11 try {
12 const response = await fetch(url, { ...mergeOptions });
13
14 if (!response.ok) {
15 throw new Error("Failed to fetch data from API");
16 }
17
18 const json = await response.json();
19 return json;
20 } catch (error) {
21 console.error(error);
22 }
23}
Since we are calling getSession from our useEffect from our client component, we don't have access to our private variables.
1 useEffect(() => {
2 getSession();
3 }, []);
That is why instead of making a call directly to our Strapi API, we are making a call to our Route Handler within Next.js first, which will give us access to our private variables using proeccess.env
and then we can make a call to Strapi API with our private variable credentials as we can see in the example below.
1import { NextResponse } from "next/server";
2
3export async function GET(request: Request) {
4 const url = `${process.env.PRIVATE_API_URL}/api/strapi-chat/get-all-sessions`;
5 const token = process.env.PRIVATE_API_TOKEN;
6
7 try {
8 const response = await fetch(url, {
9 method: "GET",
10 headers: {
11 Authorization: `Bearer ${token}`,
12 "Content-Type": "application/json",
13 },
14 cache: "no-cache",
15 });
16
17 if (!response.ok) {
18 throw new Error("Failed to fetch data from API");
19 }
20
21 const json = await response.json();
22 console.log(json);
23 return NextResponse.json(json);
24
25 } catch (error) {
26 console.error(error);
27 }
28}
Which allows us to keep our env variable private and on the server.
In this tutorial, we looked at how to build custom routes, controllers, and services combined with third-party libraries like LangChain and Open AI to create our own implementation of ChatGPT. Then we looked at combining our Strapi backend with a Next.js project.
I hope you found this tutorial helpful, and I can't wait to see what you will build based on what you have learned here.
I want to explore how to move the functionality we built today into a Strapi Plugin. The benefit of that is that it will make it easier to share this functionality with others.
For instance, we can have an Open AI api plugin that can expose different functionality from chat to image generation and is controlled from within our Strapi admin but allows us access via API endpoints.
That way, we will have access to all of our Open AI wrapped services that can be consumed by multiple applications and not just Strapi. Let me know if that is a tutorial you would be interested in.
All the best,
Paul
Strapi Backend Final GitHub Repo Next.js Frontend Final GitHub Repo