Build A personal AI chatbot with Node, Langchain, Typescript & React

Gary George
5 min readMay 1, 2024

What does this project do?

I will demonstrate how to create a chatbot capable of ingesting a resource and enabling you to retrieve data from it. For this example, I will supply a condensed version of “Romeo & Juliet,” where we can extract any existing data by querying the text.

For our sample project, we will construct:

  • A backend server using Node.js, TypeScript, Langchain, and OpenAI.
  • A front-end interface utilizing React and Chakra UI.

Our sample project is a mono repo with both the front-end and back-end in the same repository, to get started follow the readme [HERE].

The Flow

The flow for a basic chatbot is simple

1 — Set a context we wish to search against

2 — Define our query/question

3 — Vector embed the context data & the question

4 — Get a similarity search result from a vector database

5 — Pass the similarity result and our question (in plain text) to an LLM

6 — Return the result to our UI

The Server

Our server is a simple Node, Express, and Typescript setup.

Let’s take a look at the key areas of interest and how we will fulfill the flow.

1– Set a context we wish to search against

  • in our example, we have a locally stored simplified docx version of Romeo & Juliet (in production this would be a stored document in our CDN / database that we have chosen in the UI)

2 — Define our query/question

  • Our query is sent to the endpoint we have defined on /v1/in-memory-ai-text

3 — Vector embed the context data & the question

  • In our example project we will use the MemoryVectorStore provided by Langchain to do our vector embedding ( in production you would use a vector database such as Pinecone )

4 — Get a similarity search result from a vector database

  • The MemoryVectorStore will perform our similarity search and return the matches based on our query and the context data

5 — Pass the similarity result and our question in plain text to an LLM

  • We will use the Langchain loadQAStuffChain method and link it to Open Ai to complete our search for data by passing our results from MemoryVectorStore and passing in the plain text query

6 — Return the result to our UI

  • Finally, once we have the answer to our query we return it from the end-point

All of the magic happens within our server/routes/textloader.ts file.

export default async (question = "", filePath = "") => {
const fileExtension = filePath.split(".").pop();
let loader;

if (fileExtension === "docx") {
loader = new DocxLoader(filePath);
} else if (fileExtension === "txt") {
loader = new TextLoader(filePath);
} else if (fileExtension === "pdf") {
loader = new PDFLoader(filePath, {
splitPages: false,
});
} else {
return "unsupported file type";
}

const docs = await loader.load();

const vectorStore = await MemoryVectorStore.fromDocuments(
docs,
new OpenAIEmbeddings()
);

const searchResponse = await vectorStore.similaritySearch(question, 1);
const textRes = searchResponse.map((item) => item?.pageContent).join("\n");
const llm = new OpenAI({ modelName: "gpt-4" });
const chain = loadQAStuffChain(llm);

const result = await chain.invoke({
input_documents: [new Document({ pageContent: `${textRes}` })],
question,
});

console.log(`\n\n Question: ${question}`);
console.log(`\n\n Answer: ${result.text}`);
return result.text;
};

A couple of things to note, If you are using a docx file you need to install mammoth, If you are using a PDF file you will need to install pdf-parse. Both of these are already in our dependencies. They are not added automatically by Langchain.

We trigger the above function from a simple GET request, in the request, we pass the question as a query string but have hardcoded the locally stored document that will be used for the context.

To further extend this you could store your documents in something like S3 and pass in a parameter identifying the document required for the context.

export const inMemoryChat = async (req: Request, res: Response) => {
const question = req.query.question as string;
const filePath = "files/romeo&juliet.docx";

const result = await TextLoader(question, filePath);

res.status(200).json({
question,
answer: result,
});
};

We can call the backend from a rest client such as Insomnia or Postman.

Here is the result when calling our Rest API from a rest client.

The Front-end

The front end can be anything, all of the work is facilitated by the back end.

I have provided a small application using React & Chakra UI, which provides a user interface for interacting with the back end.

When you load up the app you will see this screen.

From here you can try out our chatbot, As mentioned above the scope of the queries you can make is restricted to the simplified docx version of Romeo & Juliet that we have stored locally on our server.

From here you can ask questions and see real-time answers returned from our server.

This is a great place to leave this project, ready to be explored and extended. 🚀

Project code 👨‍💻

The source code for this project is available for free [HERE].

This is a good-to-go project that can help you get a real feel for how everything works.

Helpful Links

--

--