Introduction
This tutorial will guide you through creating a chatbot that uses your documents to provide intelligent responses. We'll leverage LangChain for natural language processing, document handling, and a vector database for efficient data retrieval. The stack includes Vite for project setup, React for the front-end, TailwindCSS for styling, OpenAI for generating responses, and Supabase as our vector database. This guide aims to provide a step-by-step approach to building a chatbot from scratch using LangChain, and it will also cover how to develop a chatbot using LangChain effectively. By the end of this tutorial, you'll know how to build an LLM RAG (Retrieval-Augmented Generation) Chatbot with LangChain.
You can find the complete project code on GitHub.
Who is this for?
This tutorial is designed for developers who:
- Have basic knowledge of React and JavaScript.
- Want to build AI-powered applications using their own documents.
- Are looking to integrate vector databases and AI models into their projects.
What will be covered?
- Setting up a new project with Vite and TailwindCSS.
- Integrating LangChain with OpenAI for AI capabilities.
- Using Supabase as a vector database to store and retrieve document data.
- Building the front-end components for the chatbot.
- Creating utilities for document handling and AI response generation.
Architecture
Preparation
Create a vector store on Supabase, please refer to this GitHub Repo.
Step-by-Step Guide
Step 1: Initialize the Project with Vite
Create a new Vite project:
npm create vite@latest ai-assistant -- --template react
cd ai-assistant
Step 2: Install Necessary Dependencies
Install TailwindCSS and other dependencies:
npm install tailwindcss postcss autoprefixer react-router-dom @langchain/core @langchain/openai @supabase/supabase-js
Step 3: Configure TailwindCSS
Initialize TailwindCSS:
npx tailwindcss init -p
Configure tailwind.config.js
:
module.exports = {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}
Add Tailwind directives to src/index.css
:
@tailwind base;
@tailwind components;
@tailwind utilities;
Step 4: Configure Environment Variables
Create a .env
file in the root of your project and add your API keys:
VITE_SUPABASE_BASE_URL=your-supabase-url
VITE_SUPABASE_API_KEY=your-supabase-api-key
VITE_OPENAI_API_KEY=your-openai-api-key
VITE_OPENAI_BASE_URL=https://api.openai.com/v1
Step 5: Project Directory Structure
Organize your project files as follows:
src/
├── components/
│ ├── Avatar.jsx
│ ├── Chat.jsx
│ ├── ChatBox.jsx
│ ├── Header.jsx
│ └── Message.jsx
├── utils/
│ ├── chain.js
│ ├── combineDocuments.js
│ ├── formatConvHistory.js
│ └── retriever.js
├── App.jsx
├── main.jsx
├── index.css
└── custom.css
Step 6: Main Entry Point
src/main.jsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import './index.css'
import './custom.css'; // Custom CSS
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)
The main.jsx
file sets up the React application by rendering the App
component into the root element. It also imports the main styles, including TailwindCSS and any custom CSS.
Step 7: Creating Components
App Component
src/App.jsx
import Chat from './components/Chat'
function App() {
return (
<Chat />
)
}
export default App
The App
component serves as the root component of the application. It simply renders the Chat
component, which contains the main functionality of the AI assistant.
Chat Component
src/components/Chat.jsx
import { useState, useEffect } from 'react';
import Header from './Header';
import Message from './Message';
import ChatBox from './ChatBox';
import robotAvatar from '../assets/robot-avatar.png';
import userAvatar from '../assets/profile.jpg';
import { chain } from '../utils/chain';
import { formatConvHistory } from '../utils/formatConvHistory';
function Chat() {
const [messages, setMessages] = useState(() => {
// Retrieve messages from local storage if available
const savedMessages = localStorage.getItem('messages');
return savedMessages ? JSON.parse(savedMessages) : [];
});
const [isLoading, setIsLoading] = useState(false);
// Save messages to local storage whenever they change
useEffect(() => {
localStorage.setItem('messages', JSON.stringify(messages));
}, [messages]);
// Handle new messages sent by the user
const handleNewMessage = async (text) => {
const newMessage = {
time: new Date().toLocaleTimeString(),
text,
avatarSrc: userAvatar,
avatarAlt: "User's avatar",
position: "left",
isRobot: false,
};
setMessages((prevMessages) => [...prevMessages, newMessage]);
const updatedMessages = [...messages, newMessage];
setIsLoading(true);
try {
// Invoke the LangChain API to get the AI's response
const response = await chain.invoke({
question: text,
conv_history: formatConvHistory(updatedMessages.map(msg => msg.text)),
});
const aiMessage = {
time: new Date().toLocaleTimeString(),
text: response,
avatarSrc: robotAvatar,
avatarAlt: "Robot's avatar",
position: "right",
isRobot: true,
};
setMessages((prevMessages) => [...prevMessages, aiMessage]);
} catch (error) {
console.error("Error fetching AI response:", error);
} finally {
setIsLoading(false);
}
};
return (
<main className="font-merriweather px-10 py-8 mx-auto w-full bg-sky-950 max-w-[480px] h-screen">
<Header
mainIconSrc={robotAvatar}
mainIconAlt="Main icon"
title="AI-Assistant"
/>
<div id="chatbot-conversation-container" className="flex flex-col gap-y-2 mt-4">
{messages.map((message, index) => (
<Message
key={index}
time={message.time}
text={message.text}
avatarSrc={message.avatarSrc}
avatarAlt={message.avatarAlt}
position={message.position}
isRobot={message.isRobot}
/>
))}
</div>
<div className="mt-auto mb-4">
<ChatBox
label="What's happening?"
buttonText="Ask"
onSubmit={handleNewMessage}
isLoading={isLoading}
/>
</div>
</main>
);
}
export default Chat;
The Chat
component is the core of the AI assistant, handling message state and interaction logic.
-
useState
is used to initialize and manage themessages
state, which holds the chat messages, and theisLoading
state, which indicates if a message is being processed. - The
useEffect
hook ensures that messages are saved to local storage whenever themessages
state changes. This allows chat history to persist across page reloads. - The
handleNewMessage
function processes new messages sent by the user. It creates a new message object, updates the state with the new message, and sends the message to the LangChain API to get a response. - The AI response is then added to the messages state, updating the chat interface with both the user and AI messages.
- The
return
statement defines the layout of the chat interface, including the header, message list, and input box. TheHeader
component displays the chat title and main icon, while theMessage
component renders each message in the chat history. TheChatBox
component provides an input field and button for sending new messages.
Header Component
src/components/Header.jsx
import React from "react";
import PropTypes from "prop-types";
import Avatar from "./Avatar";
function Header({ mainIconSrc, mainIconAlt, title }) {
return (
<header className="flex flex-col mx-auto items-center grow shrink-0 basis-0 w-fit">
<Avatar src={mainIconSrc} alt={mainIconAlt} className="aspect-square w-[129px]" />
<h1 className="mt-3 text-4xl text-center text-stone-200">{title}</h1>
</header>
);
}
Header.propTypes = {
mainIconSrc: PropTypes.string.isRequired,
mainIconAlt: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
};
export default Header;
The Header
component displays the title and main icon of the chat application.
Message Component
src/components/Message.jsx
import PropTypes from "prop-types";
import Avatar from "./Avatar";
const avatarStyles = "rounded-full shrink-0 self-end aspect-square w-[47px]";
const messageTimeStyles = "self-center mt-5 text-xs font-bold leading-4 text-slate-600";
function Message({ text, time, avatarSrc, avatarAlt, position, isRobot }) {
const messageTextStyles = `text-sm leading-4 rounded-xl p-3 mt-2 ${isRobot ? "bg-sky-50 black-800" : "bg-cyan-800 text-white"
} font-merriweather`; // Apply Merriweather font
return (
<section className={`flex gap-2 ${position === 'left' ? 'justify-start' : 'justify-end'} items-center`}>
{position === 'left' && <Avatar src={avatarSrc} alt={avatarAlt} className={avatarStyles} />}
<div className="flex flex-col grow shrink-0 basis-0 w-fit">
{time && <time className={messageTimeStyles}>{time}</time>}
<p className={messageTextStyles}>{text}</p>
</div>
{position === 'right' && <Avatar src={avatarSrc} alt={avatarAlt} className={avatarStyles} />}
</section>
);
}
Message.propTypes = {
text: PropTypes.string.isRequired,
time: PropTypes.string,
avatarSrc: PropTypes.string.isRequired,
avatarAlt: PropTypes.string.isRequired,
position: PropTypes.oneOf(['left', 'right']).isRequired,
isRobot: PropTypes.bool.isRequired,
};
export default Message;
- The
Message
component displays individual messages within the chat interface. - It uses the
Avatar
component to display the avatar of the message sender. - The
messageTextStyles
are applied to style the message text, with different styles for user and robot messages.
ChatBox Component
src/components/ChatBox.jsx
import { useState } from "react";
import PropTypes from "prop-types";
const inputStyles = "w-full px-4 pt-4 pb-4 mt-2 text-base leading-5 bg-sky-900 rounded-xl border-4 border-solid shadow-sm border-slate-600 text-gray-100 resize-none";
const buttonStyles = "w-full px-6 py-3 mt-3 text-2xl font-bold text-center text-white whitespace-nowrap rounded-xl bg-slate-600 hover:bg-slate-700 hover:translate-y-0.5 focus:outline-none ";
const buttonDisabledStyles = "w-full px-6 py-3 mt-3 text-2xl font-bold text-center text-white whitespace-nowrap rounded-xl bg-slate-600 opacity-50 cursor-not-allowed";
function ChatBox({ label, buttonText, onSubmit, isLoading }) {
const [message, setMessage] = useState("");
const handleSubmit = (event) => {
event.preventDefault();
if (message.trim() && !isLoading) {
onSubmit(message);
setMessage('');
}
};
return (
<section className="flex flex-col mt-4">
<form onSubmit={handleSubmit}>
<label htmlFor="chatInput" className="sr-only">{label}</label>
<input
id="chatInput"
placeholder={label}
className={inputStyles}
value={message}
onChange={(e) => setMessage(e.target.value)}
/>
<button
type="submit"
className={isLoading ? buttonDisabledStyles : buttonStyles}
disabled={isLoading}
>
{buttonText}
</button>
</form>
</section>
);
}
ChatBox.propTypes = {
label: PropTypes.string.isRequired,
buttonText: PropTypes.string.isRequired,
onSubmit: PropTypes.func.isRequired,
isLoading: PropTypes.bool.isRequired,
};
export default ChatBox;
- The
ChatBox
component provides an input box and a button for users to send messages. - It uses
useState
to manage the message input state. - The
handleSubmit
function handles the form submission, invoking theonSubmit
function with the message content.
Avatar Component
src/components/Avatar.jsx
import React from "react";
import PropTypes from "prop-types";
function Avatar({ src, alt, className }) {
return <img loading="lazy" src={src} alt={alt} className={className} />;
}
Avatar.propTypes = {
src: PropTypes.string.isRequired,
alt: PropTypes.string.isRequired,
className: PropTypes.string,
};
export default Avatar;
The Avatar
component renders an avatar image.
Step 8: Utility Functions
Chain Utility
src/utils/chain.js
import {
RunnablePassthrough,
RunnableSequence,
} from "@langchain/core/runnables";
import { ChatOpenAI } from "@langchain/openai";
import { StringOutputParser } from "@langchain/core/output_parsers";
import { PromptTemplate } from "@langchain/core/prompts"
import { retriever } from './retriever';
import { combineDocuments } from './combineDocuments';
const openAIApiKey = import.meta.env.VITE_OPENAI_API_KEY;
const openAIUrl = import.meta.env.VITE_OPENAI_BASE_URL;
const llm = new ChatOpenAI({
apiKey: openAIApiKey,
configuration: {
baseURL: openAIUrl,
}
});
// A string holding the phrasing of the prompt
const standaloneQuestionTemplate = `Given some conversation history (if any) and a question,
convert the question to a standalone question.
conversation history: {conv_history}
question: {question}
standalone question:`;
// A prompt created using PromptTemplate and the fromTemplate method
const standaloneQuestionPrompt = PromptTemplate.fromTemplate(standaloneQuestionTemplate);
const answerTemplate = `You are a helpful and enthusiastic support bot who can answer a given question based on
the context provided and the conversation history. Try to find the answer in the context. If the answer is not given
in the context, find the answer in the conversation history if possible. If you really don't know the answer,
say "I'm sorry, I don't know the answer to that." And direct the questioner to email help@example.com.
Don't try to make up an answer. Always speak as if you were chatting to a friend.
context: {context}
conversation history: {conv_history}
question: {question}
answer: `;
const answerPrompt = PromptTemplate.fromTemplate(answerTemplate);
// Take the standaloneQuestionPrompt and PIPE the model
const standaloneQuestionChain = standaloneQuestionPrompt
.pipe(llm)
.pipe(new StringOutputParser());
const retrieverChain = RunnableSequence.from([
prevResult => prevResult.standalone_question,
retriever,
combineDocuments,
]);
const answerChain = answerPrompt
.pipe(llm)
.pipe(new StringOutputParser());
const logConvHistory = async (input) => {
console.log('Conversation History:', input.conv_history);
return input;
}
const chain = RunnableSequence.from([
{
standalone_question: standaloneQuestionChain,
original_input: new RunnablePassthrough(),
},
{
context: retrieverChain,
question: ({ original_input }) => original_input.question,
conv_history: ({ original_input }) => original_input.conv_history,
},
logConvHistory,
answerChain,
]);
export { chain };
Here is a Flowchart
This file sets up a sequence of operations to handle the user's query and retrieve an appropriate response using LangChain, OpenAI, and a retriever for context.
- StandaloneQuestionChain
: It is responsible for converting a user's query, which may depend on the context of previous conversation history, into a standalone question that can be understood without any prior context.
- RetrieverChain
: It is used to retrieve relevant documents from the vector database that are pertinent to the standalone question generated by the StandaloneQuestionChain
.
- LogConvHistory
: It is a simple logging function used to print the conversation history to the console. This can be helpful for debugging and understanding the flow of conversation and how it influences the generated responses.
- AnswerChain
: It is responsible for generating a response to the user's question based on the retrieved context and the conversation history.
Combine Documents Utility
src/utils/combineDocuments.js
export function combineDocuments(docs) {
return docs.map((doc) => doc.pageContent).join('\n\n');
}
This utility function combines multiple documents into a single string. This is useful for providing a unified context to the AI model.
Format Conversation History Utility
src/utils/formatConvHistory.js
export function formatConvHistory(messages) {
return messages.map((message, i) => {
if (i % 2 === 0) {
return `Human: ${message}`
} else {
return `AI: ${message}`
}
}).join('\n')
}
This utility function takes an array of messages
and formats them into a conversation between a human and an AI.
Retriever Utility
src/utils/retriever.js
import { SupabaseVectorStore } from "@langchain/community/vectorstores/supabase";
import { OpenAIEmbeddings } from "@langchain/openai";
import { createClient } from '@supabase/supabase-js';
const sbUrl = import.meta.env.VITE_SUPABASE_BASE_URL;
const sbApiKey = import.meta.env.VITE_SUPABASE_API_KEY;
const openAIApiKey = import.meta.env.VITE_OPENAI_API_KEY;
const openAIUrl = import.meta.env.VITE_OPENAI_BASE_URL;
const client = createClient(sbUrl, sbApiKey);
const embeddings = new OpenAIEmbeddings({
apiKey: openAIApiKey,
configuration: {
baseURL: openAIUrl
}
});
const vectorStore = new SupabaseVectorStore(embeddings, {
client,
tableName: 'personal_infos',
queryName: 'match_personal_infos',
});
const retriever = vectorStore.asRetriever();
export { retriever };
This utility sets up a retriever using Supabase and OpenAI embeddings.
- SupabaseVectorStore
: Initializes a vector store using Supabase.
- OpenAIEmbeddings
: Creates embeddings using OpenAI.
- The retriever is then used to fetch relevant documents based on the user's query.
Running the Application
-
Start the Development Server:
npm run dev
Access the Application:
Open your browser and navigate tohttp://localhost:5173
.
Conclusion
In this tutorial, you learned how to build an AI assistant using LangChain, OpenAI, and Supabase. We've covered the setup of a Vite project, integration of TailwindCSS, and the creation of React components and utility functions. You now have a functional AI assistant that can interact with users and provide responses based on the conversation history and context.
Top comments (0)