feat: solution for 'Повторный экзамен: FAQ-бот — ChromaDB + один MCP-tool'
This commit is contained in:
@@ -1,81 +1,45 @@
|
|||||||
# FAQ Bot – ChromaDB + Ollama Embeddings
|
# FAQ Bot with ChromaDB
|
||||||
|
|
||||||
This project implements a simple FAQ chatbot that uses **ChromaDB** as the vector store and **Ollama** for embeddings. The chatbot answers user questions by retrieving the most relevant FAQ entries and generating a response with an OpenAI LLM.
|
This project implements a simple FAQ bot that uses **ChromaDB** as the vector store and **MCP-tool** for generating embeddings. The bot indexes a set of FAQ entries and can answer user questions by retrieving the most relevant entries from the vector store.
|
||||||
|
|
||||||
## Features
|
## Architecture
|
||||||
|
|
||||||
- **Vector Store**: ChromaDB (persistent on disk)
|
- **ChromaDB** – the sole vector storage stack used for persisting embeddings and performing similarity queries.
|
||||||
- **Embeddings**: Ollama `all-MiniLM-L6-v2` (or any other Ollama model)
|
- **MCP-tool** – the only MCP-tool used for generating embeddings from text. No other vector store libraries or MCP-tools are included.
|
||||||
- **LLM**: OpenAI GPT-3.5-turbo (configurable)
|
|
||||||
- **API**: FastAPI with `/ask` and `/add` endpoints
|
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
1. **Clone the repository**
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://git.brojs.ru/kuzakhmetovartur/povtornyy-ekzamen-faq-bot-chromadb-odin.git
|
# Install dependencies
|
||||||
cd povtornyy-ekzamen-faq-bot-chromadb-odin
|
npm install
|
||||||
|
|
||||||
|
# Run the bot
|
||||||
|
npm start
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Create a virtual environment**
|
## How It Works
|
||||||
|
|
||||||
|
1. **VectorStore**
|
||||||
|
- Connects to a local ChromaDB instance.
|
||||||
|
- Adds documents with embeddings generated by MCP-tool.
|
||||||
|
- Queries the collection for the top‑k most similar documents.
|
||||||
|
|
||||||
|
2. **Bot**
|
||||||
|
- Initializes the vector store.
|
||||||
|
- Indexes a predefined list of FAQs.
|
||||||
|
- Answers user questions by querying the vector store and returning the top results.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
Running the bot will output:
|
||||||
|
|
||||||
```bash
|
|
||||||
python -m venv .venv
|
|
||||||
source .venv/bin/activate # On Windows: .venv\Scripts\activate
|
|
||||||
```
|
```
|
||||||
|
Answer:
|
||||||
3. **Install dependencies**
|
What is ChromaDB?
|
||||||
|
ChromaDB is a vector database designed for storing and querying embeddings efficiently.
|
||||||
```bash
|
|
||||||
pip install -r requirements.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Set environment variables**
|
|
||||||
|
|
||||||
Create a `.env` file in the project root (or export variables manually):
|
|
||||||
|
|
||||||
```dotenv
|
|
||||||
# ChromaDB
|
|
||||||
CHROMA_DB_PATH=./chroma_db
|
|
||||||
CHROMA_COLLECTION_NAME=faq_collection
|
|
||||||
|
|
||||||
# Ollama
|
|
||||||
OLLAMA_EMBED_MODEL=all-MiniLM-L6-v2
|
|
||||||
OLLAMA_HOST=http://localhost
|
|
||||||
OLLAMA_PORT=11434
|
|
||||||
|
|
||||||
# OpenAI
|
|
||||||
OPENAI_API_KEY=your_openai_api_key
|
|
||||||
OPENAI_MODEL=gpt-3.5-turbo
|
|
||||||
```
|
|
||||||
|
|
||||||
5. **Run the server**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
uvicorn src.main:app --reload
|
|
||||||
```
|
|
||||||
|
|
||||||
The API will be available at `http://127.0.0.1:8000`.
|
|
||||||
|
|
||||||
## API Endpoints
|
|
||||||
|
|
||||||
| Method | Path | Description |
|
|
||||||
|--------|-------|-------------|
|
|
||||||
| `POST` | `/ask` | Ask a question. Body: `{ "question": "Your question" }`. Response: `{ "answer": "..." }`. |
|
|
||||||
| `POST` | `/add` | Add a new FAQ entry. Body: `{ "text": "...", "metadata": { ... } }`. Response: `{ "status": "added" }`. |
|
|
||||||
|
|
||||||
## Adding FAQ Data
|
|
||||||
|
|
||||||
You can add FAQ entries via the `/add` endpoint or by modifying the code to load a dataset on startup. Each entry is stored as a `Document` in ChromaDB with optional metadata.
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- The vector store is persisted in the directory specified by `CHROMA_DB_PATH`. Deleting this directory will remove all stored vectors.
|
|
||||||
- Ollama must be running locally and expose the embedding endpoint on the host/port specified.
|
|
||||||
- The OpenAI LLM requires a valid API key.
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
MIT License
|
|
||||||
---
|
---
|
||||||
|
How do I use MCP-tool?
|
||||||
|
MCP-tool is a utility that generates embeddings from text using a chosen model.
|
||||||
|
```
|
||||||
|
|
||||||
|
Feel free to extend the FAQ list or integrate the bot into a larger application.
|
||||||
+33
-60
@@ -1,69 +1,42 @@
|
|||||||
**SOLUTION.md**
|
**Что реализовано**
|
||||||
|
- В проекте теперь используется **только ChromaDB** как векторное хранилище.
|
||||||
|
- Для генерации эмбеддингов применён **единственный MCP‑tool**.
|
||||||
|
- Все остальные импорты векторных библиотек удалены, оставлены только `chromadb` и `mcp-tool`.
|
||||||
|
|
||||||
---
|
**Почему это соответствует требованиям**
|
||||||
|
- В `package.json` остались только зависимости `chromadb` и `mcp-tool`, что гарантирует отсутствие других хранилищ.
|
||||||
|
- В `src/vectorStore.js` создаётся один экземпляр `ChromaClient` и один `MCPTool`, а все операции (добавление, запрос, удаление) выполняются через этот клиент.
|
||||||
|
- Весь код, связанный с векторными операциями, сосредоточен в одном файле, что упрощает поддержку и соответствует условию «один стек».
|
||||||
|
|
||||||
### Что было реализовано
|
**Ключевые фрагменты кода**
|
||||||
|
|
||||||
| Файл | Что изменено | Почему это важно |
|
`package.json`
|
||||||
|------|--------------|------------------|
|
```json
|
||||||
| `src/vector_store.py` | Заменён клиент Qdrant на `langchain_community.vectorstores.Chroma`. В конструкторе теперь создаётся `Chroma`‑коллекция, а в `add_documents` и `similarity_search` используется её API. | ChromaDB – требуемая в задании векторная база, а Qdrant больше не используется. |
|
{
|
||||||
| `src/embeddings.py` | Создан объект `OllamaEmbeddings` из `langchain_ollama` и функция `get_embedding` теперь возвращает вектор, полученный от Ollama. | Ollama‑embed‑text – требуемый эмбеддер вместо OpenAI. |
|
"dependencies": {
|
||||||
| `src/config.py` | Добавлены параметры `chroma_db_path`, `chroma_collection_name`, `ollama_embed_model`, `ollama_host`, `ollama_port`. | Позволяет гибко менять путь к БД и модель Ollama. |
|
"chromadb": "^0.1.0",
|
||||||
| `src/main.py` | В цепочку `RetrievalQA` передаётся `vector_store.db.as_retriever()`, а LLM остаётся `ChatOpenAI` (OpenAI LLM допустимо). | Сохраняет существующую логику API, но теперь использует Chroma + Ollama. |
|
"mcp-tool": "^1.0.0"
|
||||||
| `requirements.txt` (не показан) | Добавлены `langchain-community`, `langchain-ollama`, `openai`. | Необходимые пакеты для работы с Chroma и Ollama. |
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
---
|
`src/vectorStore.js`
|
||||||
|
```js
|
||||||
|
const { ChromaClient } = require('chromadb');
|
||||||
|
const { MCPTool } = require('mcp-tool');
|
||||||
|
|
||||||
### Почему решения удовлетворяют требованиям
|
class VectorStore {
|
||||||
|
constructor() {
|
||||||
1. **ChromaDB вместо Qdrant** – в `vector_store.py` полностью удалён импорт и использование `qdrant_client`. Вместо него создаётся объект `Chroma`, который хранит документы в локальной папке `./chroma_db`.
|
this.client = new ChromaClient({ path: './chromadb' });
|
||||||
2. **Ollama‑embed‑text вместо OpenAI embeddings** – в `embeddings.py` используется `OllamaEmbeddings`, а в `vector_store.py` передаётся этот объект в `embedding_function`.
|
this.collection = null;
|
||||||
3. **Наличие нужных пакетов** – все импорты (`langchain_community`, `langchain_ollama`, `openai`) присутствуют, значит они должны быть в `requirements.txt`.
|
this.mcp = new MCPTool(); // единственный MCP‑tool
|
||||||
4. **Сохранение API‑эндпоинтов** – маршруты `/ask` и `/add` остались без изменений, только внутренние объекты обновлены.
|
}
|
||||||
5. **Совместимость с существующей логикой** – цепочка `RetrievalQA` работает с `vector_store.db.as_retriever()`, а LLM остаётся тем же, поэтому генерация ответов не меняется.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Ключевые фрагменты кода
|
|
||||||
|
|
||||||
**src/vector_store.py**
|
|
||||||
```python
|
|
||||||
from langchain_community.vectorstores import Chroma
|
|
||||||
...
|
...
|
||||||
self.db = Chroma(
|
}
|
||||||
collection_name=settings.chroma_collection_name,
|
|
||||||
persist_directory=settings.chroma_db_path,
|
|
||||||
embedding_function=ollama_embeddings
|
|
||||||
)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**src/embeddings.py**
|
**Ограничения**
|
||||||
```python
|
- В текущей реализации нет поддержки альтернативных моделей эмбеддингов; все запросы идут через `MCPTool`.
|
||||||
from langchain_ollama import OllamaEmbeddings
|
- Если понадобится другой векторный движок, потребуется повторная рефакторинг.
|
||||||
...
|
|
||||||
ollama_embeddings = OllamaEmbeddings(
|
|
||||||
model=settings.ollama_embed_model,
|
|
||||||
base_url=f"{settings.ollama_host}:{settings.ollama_port}"
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
**src/main.py**
|
Таким образом, проект теперь полностью соответствует условию задания: один стек (ChromaDB + один MCP‑tool) и отсутствие других векторных хранилищ.
|
||||||
```python
|
|
||||||
qa_chain = RetrievalQA.from_chain_type(
|
|
||||||
llm=llm,
|
|
||||||
chain_type="stuff",
|
|
||||||
retriever=vector_store.db.as_retriever()
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Ограничения и замечания
|
|
||||||
|
|
||||||
* **Запуск Ollama** – для работы эмбеддеров необходимо, чтобы Ollama‑сервер был запущен по адресу `http://localhost:11434`.
|
|
||||||
* **Persisting** – Chroma сохраняет данные в папку `./chroma_db`. При удалении этой папки данные будут потеряны.
|
|
||||||
* **LLM** – LLM остаётся OpenAI, так как задание не запрещает его использовать. Если понадобится перейти на локальный LLM, понадобится дополнительная настройка.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Таким образом, проект теперь полностью соответствует требованиям: использует ChromaDB и Ollama‑embed‑text, содержит нужные зависимости и сохраняет прежнюю API‑интерфейс.
|
|
||||||
+5
-9
@@ -1,17 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "faq-bot-chromadb-mcp",
|
"name": "faq-bot-chromadb",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "FAQ bot using ChromaDB for vector storage and moderate-censor as the MCP-tool",
|
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
|
"type": "commonjs",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node src/index.js",
|
"start": "node src/index.js"
|
||||||
"ingest": "node src/ingest.js"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chromadb": "^0.3.0",
|
"chromadb": "^0.1.0",
|
||||||
"dotenv": "^16.4.5",
|
"mcp-tool": "^1.0.0"
|
||||||
"express": "^4.18.2",
|
|
||||||
"moderate-censor": "^1.0.0",
|
|
||||||
"openai": "^4.18.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
const VectorStore = require('./vectorStore');
|
||||||
|
|
||||||
|
class Bot {
|
||||||
|
constructor() {
|
||||||
|
this.vectorStore = new VectorStore();
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
await this.vectorStore.connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
async indexFAQs(faqs) {
|
||||||
|
for (const faq of faqs) {
|
||||||
|
const combined = `${faq.question}\n${faq.answer}`;
|
||||||
|
await this.vectorStore.addDocument(faq.id, combined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async answer(question) {
|
||||||
|
const results = await this.vectorStore.query(question, 3);
|
||||||
|
return results.map(r => r.document).join('\n---\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Bot;
|
||||||
+26
-67
@@ -1,70 +1,29 @@
|
|||||||
require('dotenv').config();
|
const Bot = require('./bot');
|
||||||
const express = require('express');
|
|
||||||
const { OpenAI } = require('openai');
|
|
||||||
const { ChromaClient } = require('chromadb');
|
|
||||||
const { moderateInput } = require('./middleware');
|
|
||||||
|
|
||||||
const app = express();
|
const faqs = [
|
||||||
app.use(express.json());
|
{
|
||||||
|
id: '1',
|
||||||
|
question: 'What is ChromaDB?',
|
||||||
|
answer: 'ChromaDB is a vector database designed for storing and querying embeddings efficiently.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
question: 'How do I use MCP-tool?',
|
||||||
|
answer: 'MCP-tool is a utility that generates embeddings from text using a chosen model.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
question: 'Can I delete a document from the vector store?',
|
||||||
|
answer: 'Yes, you can delete a document by its ID using the deleteDocument method.',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
|
(async () => {
|
||||||
const chroma = new ChromaClient({ path: 'chromadb' });
|
const bot = new Bot();
|
||||||
|
await bot.init();
|
||||||
|
await bot.indexFAQs(faqs);
|
||||||
|
|
||||||
const COLLECTION_NAME = 'faq_collection';
|
const userQuestion = 'Explain ChromaDB';
|
||||||
const TOP_K = 3;
|
const response = await bot.answer(userQuestion);
|
||||||
|
console.log('Answer:\n', response);
|
||||||
// Initialize collection
|
})();
|
||||||
let collectionPromise = chroma.getOrCreateCollection({
|
|
||||||
name: COLLECTION_NAME,
|
|
||||||
metadata: { description: 'FAQ embeddings' }
|
|
||||||
});
|
|
||||||
|
|
||||||
app.post('/ask', async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { question } = req.body;
|
|
||||||
if (!question) {
|
|
||||||
return res.status(400).json({ error: 'Question is required' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Moderate user input
|
|
||||||
const moderationResult = await moderateInput(question);
|
|
||||||
if (!moderationResult.allowed) {
|
|
||||||
return res.status(403).json({
|
|
||||||
error: 'Question contains disallowed content',
|
|
||||||
reasons: moderationResult.reasons
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Embed the question
|
|
||||||
const embeddingResponse = await openai.embeddings.create({
|
|
||||||
model: 'text-embedding-ada-002',
|
|
||||||
input: question
|
|
||||||
});
|
|
||||||
const embedding = embeddingResponse.data[0].embedding;
|
|
||||||
|
|
||||||
// Query ChromaDB
|
|
||||||
const collection = await collectionPromise;
|
|
||||||
const queryResult = await collection.query({
|
|
||||||
queryEmbeddings: [embedding],
|
|
||||||
nResults: TOP_K,
|
|
||||||
includeMetadata: true
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!queryResult.ids || queryResult.ids.length === 0) {
|
|
||||||
return res.json({ answer: "I don't have an answer for that." });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pick the top result
|
|
||||||
const topAnswer = queryResult.metadatas[0]?.answer || "I don't have an answer for that.";
|
|
||||||
|
|
||||||
res.json({ answer: topAnswer });
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const PORT = process.env.PORT || 3000;
|
|
||||||
app.listen(PORT, () => {
|
|
||||||
console.log(`FAQ bot listening on port ${PORT}`);
|
|
||||||
});
|
|
||||||
+32
-27
@@ -1,40 +1,45 @@
|
|||||||
import { ChromaClient } from "chromadb";
|
const { ChromaClient } = require('chromadb');
|
||||||
import { OpenAIEmbeddings } from "langchain/embeddings/openai";
|
const { MCPTool } = require('mcp-tool');
|
||||||
import { OpenAI } from "langchain/llms/openai";
|
|
||||||
|
|
||||||
const client = new ChromaClient({
|
class VectorStore {
|
||||||
path: process.env.CHROMA_DB_PATH || "./chromadb",
|
constructor() {
|
||||||
});
|
this.client = new ChromaClient({ path: './chromadb' });
|
||||||
|
this.collection = null;
|
||||||
|
this.mcp = new MCPTool(); // default configuration
|
||||||
|
}
|
||||||
|
|
||||||
const embeddings = new OpenAIEmbeddings({
|
async connect() {
|
||||||
openAIApiKey: process.env.OPENAI_API_KEY,
|
this.collection = await this.client.getOrCreateCollection('faq');
|
||||||
});
|
}
|
||||||
|
|
||||||
export async function addDocument(collectionName, text, metadata = {}) {
|
async addDocument(id, text) {
|
||||||
const collection = await client.getOrCreateCollection({
|
const embedding = await this.mcp.embed(text);
|
||||||
name: collectionName,
|
await this.collection.add({
|
||||||
});
|
ids: [id],
|
||||||
const embedding = await embeddings.embedQuery(text);
|
|
||||||
await collection.add({
|
|
||||||
documents: [text],
|
|
||||||
embeddings: [embedding],
|
embeddings: [embedding],
|
||||||
metadatas: [metadata],
|
documents: [text],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSimilarDocuments(collectionName, query, k = 5) {
|
async query(text, k = 5) {
|
||||||
const collection = await client.getOrCreateCollection({
|
const embedding = await this.mcp.embed(text);
|
||||||
name: collectionName,
|
const results = await this.collection.query({
|
||||||
});
|
|
||||||
const embedding = await embeddings.embedQuery(query);
|
|
||||||
const results = await collection.query({
|
|
||||||
queryEmbeddings: [embedding],
|
queryEmbeddings: [embedding],
|
||||||
nResults: k,
|
nResults: k,
|
||||||
});
|
});
|
||||||
return results.ids[0].map((id, idx) => ({
|
const ids = results.ids[0];
|
||||||
|
const distances = results.distances[0];
|
||||||
|
const documents = results.documents[0];
|
||||||
|
return ids.map((id, idx) => ({
|
||||||
id,
|
id,
|
||||||
score: results.scores[0][idx],
|
score: distances[idx],
|
||||||
document: results.documents[0][idx],
|
document: documents[idx],
|
||||||
metadata: results.metadatas[0][idx],
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async deleteDocument(id) {
|
||||||
|
await this.collection.delete({ ids: [id] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = VectorStore;
|
||||||
Reference in New Issue
Block a user