Building a Chatbot using your documents with LangChain from Scratch
Tags: chatbot, openai, rag, vectordatabase
Published: 2024-06-24 14:09:10

## 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](https://github.com/jacky-xbb/ai-assistant).***

## 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

![LLM RAG Architecture](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nqty66qpgha3et66izn3.png)


## Preparation

Create a vector store on Supabase, please refer to this GitHub [Repo](https://github.com/jacky-xbb/supabase-vector-py).

## Step-by-Step Guide

### Step 1: Initialize the Project with Vite

Create a new Vite project:

```bash
npm create vite@latest ai-assistant -- --template react
cd ai-assistant
```

### Step 2: Install Necessary Dependencies

Install TailwindCSS and other dependencies:

```bash
npm install tailwindcss postcss autoprefixer react-router-dom @langchain/core @langchain/openai @supabase/supabase-js
```

### Step 3: Configure TailwindCSS

Initialize TailwindCSS:

```bash
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`:

```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

```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

```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

```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 the `messages` state, which holds the chat messages, and the `isLoading` state, which indicates if a message is being processed.
- The `useEffect` hook ensures that messages are saved to local storage whenever the `messages` 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. The `Header` component displays the chat title and main icon, while the `Message` component renders each message in the chat history. The `ChatBox` component provides an input field and button for sending new messages.

**Header Component**

src/components/Header.jsx

```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

```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

```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 the `onSubmit` function with the message content.

**Avatar Component**

src/components/Avatar.jsx

```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

```jsx
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


![flowchart for chain](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7mglvjt5sr0rvx7tmld1.png)

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

```jsx
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

```jsx
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

```jsx
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

1. **Start the Development Server:**
    
    ```bash
    npm run dev
    ```
    
2. **Access the Application:**
Open your browser and navigate to `http://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.

## References

- [GitHub Repo](https://github.com/jacky-xbb/ai-assistant)
- [Install Tailwind CSS with Vite](https://tailwindcss.com/docs/guides/vite)
- [LangChain Documentation](https://js.langchain.com/v0.2/docs/introduction/)
- [Supabase Vector Documentation](https://supabase.com/docs/guides/ai)