Skip to content

Commit

Permalink
Add local ai bot
Browse files Browse the repository at this point in the history
  • Loading branch information
fmenezes committed Jan 23, 2025
1 parent 8b37f25 commit 28ad1d9
Show file tree
Hide file tree
Showing 5 changed files with 421 additions and 0 deletions.
7 changes: 7 additions & 0 deletions apps/local-bot/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
FROM python:3.12
WORKDIR /opt/app
ADD requirements.txt .
RUN pip install -r requirements.txt
ADD app.py .

CMD ["streamlit", "run", "app.py", "--server.port=8501", "--server.address=0.0.0.0"]
279 changes: 279 additions & 0 deletions apps/local-bot/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
# Step-by-Step Guide: Building a Local Chatbot with Streamlit, LangChain, Ollama, and MongoDB Atlas

In this tutorial, we'll set up a local chatbot using **Streamlit**, **LangChain**, **Ollama**, and **MongoDB Atlas Search**. This bot will leverage MongoDB's powerful Atlas Search capabilities alongside local LLMs (Large Language Models) via Ollama, allowing you to enhance user queries with context from chat history.

## Prerequisites
Before starting, make sure you have the following installed:

* Docker
* Docker Compose

> [!NOTE]
> For this tutorial, Docker is essential for containerized, isolated development.
## Step 1: Setting Up the Project

### Project Overview
We’ll start by creating a directory for the project files and setting up our working structure.

```sh
mkdir localai
cd localai
```

### Project Structure

Organize your project files as shown:

```
localai/
├── app.py
├── Dockerfile
├── compose.yaml
└── requirements.txt
```

### Tool Overview

Here’s a quick rundown of the tools we’re using in this project:

* *[Streamlit](https://streamlit.io)*: A Python library for easily creating data-based web applications. We'll use it to create a local chatbot interface.
* *[LangChain](https://langchain.com)*: A framework that simplifies working with LLMs and document processing. It will assist processing user queries and generate responses.
* *[Ollama](https://ollama.com)*: A solution for deploying LLMs locally without external API dependency. It to host our models.
* *[MongoDB Atlas Search](https://www.mongodb.com/products/platform/atlas-search)*: Adds a powerful, flexible vector search functionality to our app. It will store user queries and responses in MongoDB.

### Setting Up `requirements.txt`

In `requirements.txt`, specify the dependencies needed for this project:

```requirements.txt
streamlit
ollama
langchain
langchain_ollama
pymongo
langchain_mongodb
langchain_community
markdownify
```

### Docker Configuration

#### `Dockerfile`
Create a Dockerfile and add the following content. This file will define the container setup, ensuring our app and its dependencies run consistently across environments.

```Dockerfile
FROM python:3.12
WORKDIR /opt/app
ADD requirements.txt .
RUN pip install -r requirements.txt
ADD app.py .

CMD ["streamlit", "run", "app.py", "--server.port=8501", "--server.address=0.0.0.0"]
```

#### `compose.yaml`
Define your Docker Compose configuration in `compose.yaml`:

```yaml
services:
app:
build:
context: .
ports:
- 8501:8501/tcp
environment:
MONGO_URI: mongodb://root:root@mongo:27017/admin?directConnection=true
ollama:
image: ollama/ollama
mongo:
image: mongodb/mongodb-atlas-local
environment:
- MONGODB_INITDB_ROOT_USERNAME=root
- MONGODB_INITDB_ROOT_PASSWORD=root
ports:
- 27017:27017
```
> [!TIP]
> If you are on macOS, install Ollama locally and use this modified version of `compose.yaml`:
> ```yaml
> services:
> app:
> build:
> context: .
> ports:
> - 8501:8501/tcp
> environment:
> OLLAMA_HOST: host.docker.internal:11434
> MONGO_URI: mongodb://root:root@mongo:27017/admin?directConnection=true
> extra_hosts:
> - "host.docker.internal:host-gateway"
> mongo:
> image: mongodb/mongodb-atlas-local
> environment:
> - MONGODB_INITDB_ROOT_USERNAME=root
> - MONGODB_INITDB_ROOT_PASSWORD=root
> ports:
> - 27017:27017
> ```

## Step 2: Creating the Initial App

Create app.py with a simple “Hello World” message to ensure your environment is set up correctly.

```python
import streamlit as st
st.write('Hello, World!')
```

With all files in place, you can now build and run the Docker containers:

```sh
docker compose up --build
```

Access the app at http://localhost:8501 in your browser. You should see the message Hello, World!

> _Expected Output_: A browser page displaying “Hello, World!” confirms your setup is correct.

## Step 3: Build the Chatbot

### Step 3.1: Setting Up MongoDB and Ollama

In `app.py`, connect to MongoDB and Ollama. This will pull LLM models from Ollama and set up MongoDB for data storage.

```python
import os
import streamlit as st
from pymongo import MongoClient
import ollama
# Model and embedding configurations
MODEL = "llama3.2"
EMBEDDING_MODEL = "nomic-embed-text"
MONGO_URI = os.getenv("MONGO_URI", "mongodb://localhost:27017")
# Pull models from Ollama
ollama.pull(MODEL)
ollama.pull(EMBEDDING_MODEL)
# Initialize MongoDB client
try:
mongo_client = MongoClient(MONGO_URI)
collection = mongo_client["bot"]["data"]
except Exception as e:
st.error(f"Failed to connect to MongoDB: {e}")
st.stop()
```

> [!NOTE]
> After each step, you can test the app by re-running `docker compose up --build`.

### Step 3.2: Loading Documents and Creating a Vector Search Index

Now, load documents, process them with LangChain, and store them as vector embeddings in MongoDB. This setup allows MongoDB Atlas to perform fast vector-based searches.

```python
from langchain_ollama import OllamaEmbeddings
from langchain_community.document_loaders import WebBaseLoader
from langchain_community.document_transformers import MarkdownifyTransformer
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_mongodb import MongoDBAtlasVectorSearch
embedding = OllamaEmbeddings(model=EMBEDDING_MODEL)
collection.drop()
loaders = [
WebBaseLoader("https://en.wikipedia.org/wiki/AT%26T"),
WebBaseLoader("https://en.wikipedia.org/wiki/Bank_of_America")
]
docs = []
for loader in loaders:
for doc in loader.load():
docs.append(doc)
md = MarkdownifyTransformer()
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
converted_docs = md.transform_documents(docs)
splits = text_splitter.split_documents(converted_docs)
vectorstore = MongoDBAtlasVectorSearch.from_documents(splits, embedding, collection=collection, index_name="default")
vectorstore.create_vector_search_index(768)
```

> _Expected Output_: After this step, MongoDB Atlas should contain indexed documents, enabling fast vector-based search capabilities.

### Step 3.3: Setting Up the Chat Model

Next, set up the chat model with a retrieval mechanism and define the chain of operations that will handle user queries.

```python
from langchain_ollama import ChatOllama
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_core.chat_history import BaseChatMessageHistory
from langchain_mongodb import MongoDBChatMessageHistory
retriever = vectorstore.as_retriever()
chat = ChatOllama(model=MODEL)
# System message for the chatbot
SYSTEM_MESSAGE = """You're a helpful assistant. Answer all questions to the best of your ability. If you don't know the answer let the user know to find help on the internet.
Available context:
{context}
"""
prompt_template = ChatPromptTemplate.from_messages([
("system", SYSTEM_MESSAGE),
MessagesPlaceholder("history"),
("human", "{input}"),
])
chain = {
"context": itemgetter("input") | retriever,
"input": itemgetter("input"),
"history": itemgetter("history")
} | prompt_template | chat | StrOutputParser()
def get_session_history(session_id: str) -> BaseChatMessageHistory:
return MongoDBChatMessageHistory(MONGO_URI, session_id, database_name="bot")
history_chain = RunnableWithMessageHistory(chain, get_session_history, input_messages_key="input", history_messages_key="history")
```

### Step 3.4: Creating the Chat Interface

Now, use Streamlit to create a chat interface for interacting with the chatbot.

```python
st.title("Chatbot")
st.caption("A Streamlit chatbot")
history = get_session_history()
for msg in history.messages:
st.chat_message(msg.type).write(msg.content)
if prompt := st.chat_input():
st.chat_message("user").write(prompt)
with st.chat_message("ai"):
with st.spinner("Thinking..."):
st.write_stream(history_chain.stream({"input": prompt}))
```

At this point, you can start prompting with inputs like “Who started AT&T?” and see the chatbot respond!

## Conclusion and Next Steps

In this tutorial, we built a local chatbot setup using MongoDB Atlas Search and local LLMs via Ollama, integrated through Streamlit. This project forms a robust foundation for further development and deployment.

Possible Extensions:

* Add more sophisticated pre-processing for documents.
* Experiment with different models in Ollama.
* Deploy to a cloud environment like AWS or Azure for production scaling.

Feel free to customize this setup to suit your needs. Happy coding!
106 changes: 106 additions & 0 deletions apps/local-bot/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import os
from operator import itemgetter

import streamlit as st
import ollama
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_ollama import ChatOllama, OllamaEmbeddings
from pymongo import MongoClient
from langchain_mongodb import MongoDBChatMessageHistory, MongoDBAtlasVectorSearch
from langchain_community.document_loaders import WebBaseLoader
from langchain_community.document_transformers import MarkdownifyTransformer
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_core.chat_history import BaseChatMessageHistory

# System message for the chatbot
SYSTEM_MESSAGE = """You're a helpful assistant. Answer all questions to the best of your ability. If you don't know the answer let the user know to find help on the internet.
Available context:
{context}
"""

# Model and embedding configurations
MODEL = "llama3.2"
EMBEDDING_MODEL = "nomic-embed-text"
MONGO_URI = os.getenv("MONGO_URI", "mongodb://localhost:27017")

# Pull models from Ollama
ollama.pull(MODEL)
ollama.pull(EMBEDDING_MODEL)

# Initialize MongoDB client
try:
mongo_client = MongoClient(MONGO_URI)
collection = mongo_client["bot"]["data"]
except Exception as e:
st.error(f"Failed to connect to MongoDB: {e}")
st.stop()

# Initialize embeddings
embedding = OllamaEmbeddings(model=EMBEDDING_MODEL)

# Load documents and create vector search index if not already present
collection.drop()

loaders = [
WebBaseLoader("https://en.wikipedia.org/wiki/AT%26T"),
WebBaseLoader("https://en.wikipedia.org/wiki/Bank_of_America")
]
docs = []
for loader in loaders:
for doc in loader.load():
docs.append(doc)
md = MarkdownifyTransformer()
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000, chunk_overlap=200)
docs = loader.load()
converted_docs = md.transform_documents(docs)
splits = text_splitter.split_documents(converted_docs)
vectorstore = MongoDBAtlasVectorSearch.from_documents(
splits, embedding, collection=collection, index_name="default")
vectorstore.create_vector_search_index(768)

# Initialize retriever and chat model
retriever = vectorstore.as_retriever()
chat = ChatOllama(model=MODEL)

# Define prompt template
prompt_template = ChatPromptTemplate.from_messages([
("system", SYSTEM_MESSAGE),
MessagesPlaceholder("history"),
("human", "{input}"),
])

# Define the chain of operations
chain = {
"context": itemgetter("input") | retriever,
"input": itemgetter("input"),
"history": itemgetter("history")
} | prompt_template | chat | StrOutputParser()

# Function to get session history
def get_session_history() -> BaseChatMessageHistory:
return MongoDBChatMessageHistory(MONGO_URI, "user", database_name="bot")


# Initialize history chain
history_chain = RunnableWithMessageHistory(
chain, get_session_history, input_messages_key="input", history_messages_key="history")

# Streamlit UI
st.title("Chatbot")
st.caption("A Streamlit chatbot")

# Display chat history
history = get_session_history()
for msg in history.messages:
st.chat_message(msg.type).write(msg.content)

# Handle user input
if prompt := st.chat_input():
st.chat_message("user").write(prompt)
with st.chat_message("ai"):
with st.spinner("Thinking..."):
st.write_stream(history_chain.stream({"input": prompt}))
Loading

0 comments on commit 28ad1d9

Please sign in to comment.