DEV Community

Cover image for Building a Simple Chatbot with LangGraph and Chainlit: A Step-by-Step Tutorial
James
James

Posted on

Building a Simple Chatbot with LangGraph and Chainlit: A Step-by-Step Tutorial

Building a Simple Chatbot with LangGraph and Chainlit: A Step-by-Step Tutorial

Hey there, fellow developers! If you've been diving into the world of AI and chatbots, you know how exciting it is to build something interactive that can respond to user queries in real-time. Today, I'm walking you through a straightforward tutorial on creating a simple chatbot using LangGraph for the graph-based workflow and Chainlit for the user interface. This setup leverages the power of large language models (LLMs) via OpenAI and OpenRouter, making it easy to prototype conversational AI apps.

I've also put together a YouTube video that demonstrates this code in action—check it out Step-by-Step Chainlit Memory Chatbot with LangGraph and OpenRouter! | Part 11 for a visual walkthrough. We'll break down the code step by step, explain what each part does, and get you up and running quickly. By the end, you'll have a functional chatbot that maintains conversation history and streams responses smoothly.

Why LangGraph and Chainlit?

Before we jump into the code, let's talk about the tools. LangGraph is a library from LangChain that lets you build stateful, graph-based applications for LLMs. It's perfect for managing conversation flows, where each "node" can represent a step like calling an AI model. Chainlit, on the other hand, is a lightweight framework for creating interactive UIs for LLM apps—think of it as a quick way to spin up a chat interface without heavy frontend work. Together, they make building and deploying chatbots a breeze, especially for prototypes or demos.

This tutorial assumes you're comfortable with Python and have some familiarity with APIs. If not, no worries—the code is simple, and I'll explain everything.

Prerequisites

To follow along, you'll need a few things set up. First, install the required libraries using pip: langgraph, langchain-openai, chainlit, and python-dotenv. You can run pip install langgraph langchain-openai chainlit python-dotenv in your terminal. You'll also need an OpenRouter API key (sign up at openrouter.ai if you don't have one) and a .env file to store it securely. Finally, make sure you have Python 3.8 or later installed.

Once that's done, create a new Python file (say, chatbot.py) and paste in the code we'll discuss. To run the app, use chainlit run chatbot.py from your command line, and it'll launch a local web interface for chatting.

Breaking Down the Code

Let's dissect the code section by section. I've organized it into setup, graph definition, compilation, and the Chainlit UI integration, just like in the original script.

Initial Setup

We start by importing the necessary modules and loading environment variables. This keeps your API keys safe and out of your codebase.

from langchain_core.messages import HumanMessage, AIMessageChunk
from langchain_core.runnables.config import RunnableConfig
from langchain_openai import ChatOpenAI

from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import START, MessagesState, StateGraph
import chainlit as cl
import os
from dotenv import load_dotenv

load_dotenv()  # Load environment variables from a .env file

openrouter_api_key = os.getenv('OPENROUTER_API_KEY')
Enter fullscreen mode Exit fullscreen mode

Here, we're pulling in tools from LangChain for handling messages and configurations, LangGraph for building the graph, and Chainlit for the UI. The load_dotenv() call reads your .env file, where you should have something like OPENROUTER_API_KEY=your_key_here and optionally OPENROUTER_BASE_URL=https://openrouter.ai/api/v1. This setup ensures your app can securely access the OpenRouter API, which acts as a gateway to models like Google's Gemini.

Defining the Graph

Next, we define the core workflow using LangGraph. This is where the magic happens—we create a simple graph that calls an LLM and updates the conversation state.

workflow = StateGraph(state_schema=MessagesState)

model = ChatOpenAI(
    model='google/gemini-2.5-flash-lite',
    base_url=os.getenv('OPENROUTER_BASE_URL', 'https://openrouter.ai/api/v1'),
    api_key=openrouter_api_key,
    temperature=0,  # Set for deterministic, less creative responses
)

def call_model(state: MessagesState):
    """Invokes the model with the current state and returns the new message."""
    response = model.invoke(state['messages'])
    return {'messages': response}  # Update the state with the model's response

workflow.add_node('model', call_model)  # Add the model-calling function as a node
workflow.add_edge(START, 'model')  # Set the 'model' node as the entry point
Enter fullscreen mode Exit fullscreen mode

We initialize a StateGraph with MessagesState, which is a built-in schema for storing conversation history as a list of messages. Then, we set up the ChatOpenAI model, pointing it to Gemini via OpenRouter with a temperature of 0 for predictable outputs (feel free to tweak this for more creativity).

The call_model function takes the current state (which includes all previous messages), invokes the model, and returns the response to update the state. We add this as a node in the graph and connect it directly from the starting point (START). This creates a basic loop: user input goes in, model responds, and the state persists.

Compiling the Graph

With the graph defined, we compile it into a runnable app, adding memory to remember past interactions.

memory = MemorySaver()  # Initialize in-memory storage for conversation history

# Compile the graph into a runnable, adding the memory checkpointer
app = workflow.compile(checkpointer=memory)
Enter fullscreen mode Exit fullscreen mode

MemorySaver is LangGraph's way of checkpointing the state—it's in-memory here, but you could swap it for something persistent like a database for production. Compiling with checkpointer=memory ensures each conversation thread maintains its history, so the chatbot "remembers" what was said earlier.

Integrating with Chainlit UI

Finally, we hook everything up to Chainlit to create an interactive chat interface.

@cl.on_message
async def main(message: cl.Message):
    """Process incoming user messages and stream back the AI's response."""
    answer = cl.Message(content='')  # Create an empty message to stream the response into
    await answer.send()

    # Configure the runnable to associate the conversation with the user's session
    config: RunnableConfig = {'configurable': {'thread_id': cl.context.session.thread_id}}

    # Stream the graph's output
    for msg, _ in app.stream(
        {'messages': [HumanMessage(content=message.content)]},  # Pass the user's message
        config,
        stream_mode='messages',  # Stream individual message chunks
    ):
        # Check if the current streamed item is an AI message chunk
        if isinstance(msg, AIMessageChunk):
            answer.content += msg.content  # type: ignore # Append the content chunk
            await answer.update()  # Update the UI with the appended content
Enter fullscreen mode Exit fullscreen mode

The @cl.on_message decorator tells Chainlit to run this function whenever a user sends a message. We create an empty response message and send it immediately to show a loading state. Then, we configure the graph with a unique thread_id based on the session, ensuring isolated conversations.

The app.stream call runs the graph with the user's input as a HumanMessage, streaming back chunks in 'messages' mode. We check for AIMessageChunk instances (partial AI responses) and append them to the UI message, updating it in real-time. This gives that smooth, typing-like effect you see in modern chatbots.

Running and Testing Your Chatbot

Save the code in chatbot.py, ensure your .env file is set up, and run chainlit run chatbot.py. Open the provided local URL in your browser, and you'll see a clean chat interface. Type something like "Tell me a joke," and watch the response stream in. Thanks to the memory, follow-up questions will build on the context.

If you run into issues, double-check your API key and model name—OpenRouter supports various models, so experiment with others if Gemini isn't your vibe. For more advanced features, you could add tools, multiple nodes, or even conditional edges in the graph.

Wrapping Up

There you have it—a complete, working chatbot built with LangGraph and Chainlit in under 50 lines of code! This is a great starting point for more complex AI apps, like adding retrieval-augmented generation (RAG) or integrating external APIs. If you followed along, try extending it: maybe add a node for tool calls or persist memory to a file.

Don't forget to watch the companion YouTube video Step-by-Step Chainlit Memory Chatbot with LangGraph and OpenRouter! | Part 11 for a live demo and tips. If you build something cool with this, drop a comment below or share your tweaks—I'd love to hear about it. Happy coding, and see you in the next tutorial! 🚀

Top comments (0)