feat: solution for 'Повторный экзамен: FAQ-бот — ChromaDB + один MCP-tool'

This commit is contained in:
2026-06-30 00:14:56 +03:00
parent f7c159346a
commit 1b9342d225
5 changed files with 225 additions and 68 deletions
+79 -16
View File
@@ -1,21 +1,27 @@
# FAQ Bot with ChromaDB and LangChain # FAQ Bot with ChromaDB and moderate-censor
This project implements a simple FAQ bot that uses **ChromaDB** for vector storage and **LangChain** for building an intelligent agent. The bot can answer questions based on a small knowledge base stored in ChromaDB. This project implements an FAQ bot that uses **ChromaDB** for vector storage and retrieval, and **moderate-censor** as the single MCP-tool for content moderation.
## Features ## Features
- Stores documents in ChromaDB with embeddings from OpenAI. - Vector-based FAQ retrieval using OpenAI embeddings and ChromaDB.
- Uses the latest LangChain agent creation method (`initializeAgentExecutorWithOptions`). - User input moderation with moderate-censor.
- Simple CLI interface for interacting with the bot. - Simple HTTP API (`/ask`) to query the bot.
- Easy to extend with more documents or tools.
## Prerequisites
- Node.js v18+ (or any LTS version)
- npm
- OpenAI API key (set in `.env`)
- ChromaDB server running locally (default path: `chromadb`)
## Setup ## Setup
1. **Clone the repository** 1. **Clone the repository**
```bash ```bash
git clone https://github.com/your-username/faq-bot-chromadb.git git clone <repo-url>
cd faq-bot-chromadb cd <repo-directory>
``` ```
2. **Install dependencies** 2. **Install dependencies**
@@ -24,26 +30,83 @@ This project implements a simple FAQ bot that uses **ChromaDB** for vector stora
npm install npm install
``` ```
3. **Configure environment** 3. **Create a `.env` file**
Create a `.env` file in the project root:
```env ```env
OPENAI_API_KEY=your_openai_api_key OPENAI_API_KEY=your_openai_api_key
CHROMA_DB_PATH=./chromadb PORT=3000
``` ```
4. **Run the bot** 4. **Prepare FAQ data**
Create a `faq.json` file in the project root with the following format:
```json
[
{
"question": "What is ChromaDB?",
"answer": "ChromaDB is a vector database for storing and retrieving embeddings."
},
{
"question": "How do I use the bot?",
"answer": "Send a POST request to /ask with a JSON body containing the 'question' field."
}
]
```
5. **Ingest FAQ data**
```bash
npm run ingest
```
This will read `faq.json`, generate embeddings, and store them in ChromaDB.
6. **Start the bot**
```bash ```bash
npm start npm start
``` ```
You can also use `npm run dev` for automatic restarts with nodemon. The server will listen on the port specified in `.env` (default 3000).
## Adding Documents ## Usage
The bot comes with two sample FAQ entries. To add more, edit `src/index.js` or use the `addDocument` function from `src/vectorstore.js`. Send a POST request to `/ask`:
```bash
curl -X POST http://localhost:3000/ask \
-H "Content-Type: application/json" \
-d '{"question":"What is ChromaDB?"}'
```
Response:
```json
{
"answer": "ChromaDB is a vector database for storing and retrieving embeddings."
}
```
If the question contains disallowed content, the bot will respond with a 403 status and reasons.
## Project Structure
```
├── package.json
├── src
│ ├── index.js # HTTP server and bot logic
│ ├── ingest.js # FAQ ingestion script
│ └── middleware.js # Moderation middleware
├── faq.json # FAQ data file
└── README.md
```
## Notes
- The bot uses the `text-embedding-ada-002` model for embeddings.
- Only one MCP-tool (`moderate-censor`) is used as required.
- Ensure the ChromaDB server is running before ingesting data or starting the bot.
## License ## License
+6 -10
View File
@@ -1,21 +1,17 @@
{ {
"name": "faq-bot-chromadb", "name": "faq-bot-chromadb-mcp",
"version": "1.0.0", "version": "1.0.0",
"description": "FAQ bot using ChromaDB and LangChain", "description": "FAQ bot using ChromaDB for vector storage and moderate-censor as the MCP-tool",
"main": "src/index.js", "main": "src/index.js",
"type": "module",
"scripts": { "scripts": {
"start": "node src/index.js", "start": "node src/index.js",
"dev": "nodemon src/index.js" "ingest": "node src/ingest.js"
}, },
"dependencies": { "dependencies": {
"chromadb": "^0.3.0", "chromadb": "^0.3.0",
"langchain": "^0.2.0",
"langchain-community": "^0.2.0",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"openai": "^4.27.0" "express": "^4.18.2",
}, "moderate-censor": "^1.0.0",
"devDependencies": { "openai": "^4.18.0"
"nodemon": "^3.0.1"
} }
} }
+62 -42
View File
@@ -1,50 +1,70 @@
import dotenv from "dotenv"; require('dotenv').config();
import readline from "readline"; const express = require('express');
import { createAgent } from "./agent.js"; const { OpenAI } = require('openai');
import { addDocument } from "./vectorstore.js"; const { ChromaClient } = require('chromadb');
const { moderateInput } = require('./middleware');
dotenv.config(); const app = express();
app.use(express.json());
const COLLECTION = "faq_collection"; const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const chroma = new ChromaClient({ path: 'chromadb' });
async function main() { const COLLECTION_NAME = 'faq_collection';
// Optional: add some sample documents const TOP_K = 3;
await addDocument(
COLLECTION,
"What is the return policy?",
{ source: "FAQ" }
);
await addDocument(
COLLECTION,
"How can I track my order?",
{ source: "FAQ" }
);
const agent = await createAgent(COLLECTION); // Initialize collection
let collectionPromise = chroma.getOrCreateCollection({
name: COLLECTION_NAME,
metadata: { description: 'FAQ embeddings' }
});
const rl = readline.createInterface({ app.post('/ask', async (req, res) => {
input: process.stdin,
output: process.stdout,
prompt: "You: ",
});
console.log("FAQ Bot is ready. Type your question and press Enter.");
rl.prompt();
rl.on("line", async (line) => {
const question = line.trim();
if (!question) {
rl.prompt();
return;
}
try { try {
const result = await agent.call({ input: question }); const { question } = req.body;
console.log(`Bot: ${result.output}`); if (!question) {
} catch (err) { return res.status(400).json({ error: 'Question is required' });
console.error("Error:", err);
} }
rl.prompt();
});
}
main().catch((err) => console.error(err)); // 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}`);
});
+55
View File
@@ -0,0 +1,55 @@
require('dotenv').config();
const fs = require('fs');
const path = require('path');
const { OpenAI } = require('openai');
const { ChromaClient } = require('chromadb');
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const chroma = new ChromaClient({ path: 'chromadb' });
const COLLECTION_NAME = 'faq_collection';
const FAQ_FILE = path.join(__dirname, '..', 'faq.json');
async function ingest() {
try {
const rawData = fs.readFileSync(FAQ_FILE, 'utf-8');
const faqEntries = JSON.parse(rawData);
const collection = await chroma.getOrCreateCollection({
name: COLLECTION_NAME,
metadata: { description: 'FAQ embeddings' }
});
const documents = [];
const embeddings = [];
const ids = [];
const metadatas = [];
for (let i = 0; i < faqEntries.length; i++) {
const { question, answer } = faqEntries[i];
const embeddingResponse = await openai.embeddings.create({
model: 'text-embedding-ada-002',
input: question
});
const embedding = embeddingResponse.data[0].embedding;
documents.push(question);
embeddings.push(embedding);
ids.push(`faq-${i}`);
metadatas.push({ answer });
}
await collection.add({
documents,
embeddings,
ids,
metadatas
});
console.log(`Ingested ${faqEntries.length} FAQ entries into collection '${COLLECTION_NAME}'.`);
} catch (err) {
console.error('Error during ingestion:', err);
}
}
ingest();
+23
View File
@@ -0,0 +1,23 @@
const moderate = require('moderate-censor');
/**
* Moderates user input using moderate-censor.
* @param {string} text
* @returns {Promise<{allowed: boolean, reasons: string[]}>}
*/
async function moderateInput(text) {
try {
const result = await moderate.moderate(text);
if (result.isAllowed) {
return { allowed: true, reasons: [] };
} else {
return { allowed: false, reasons: result.reasons || [] };
}
} catch (err) {
console.error('Moderation error:', err);
// If moderation fails, default to allowing to avoid blocking legitimate queries
return { allowed: true, reasons: [] };
}
}
module.exports = { moderateInput };