feat: solution for 'Экзамен: RAG-агент с ChromaDB и веб-поиском'
This commit is contained in:
@@ -1,35 +1,82 @@
|
|||||||
# Экзамен: RAG-агент с ChromaDB и веб-поиском
|
# RAG Agent with ChromaDB and Web Search
|
||||||
|
|
||||||
Главная
|
This project implements a Retrieval-Augmented Generation (RAG) agent that uses **ChromaDB** as the vector store and performs web search to ingest documents. The agent answers user questions by retrieving relevant passages from the stored documents and generating responses with OpenAI’s GPT models.
|
||||||
Мои задания
|
|
||||||
Экзамен: RAG-агент с ChromaDB и веб-поиском
|
|
||||||
5Д
|
|
||||||
EN
|
|
||||||
Экзамен: RAG-агент с ChromaDB и веб-поиском
|
|
||||||
Зачёт
|
|
||||||
Версия 2
|
|
||||||
Дедлайн сдачи: 31.08.2026
|
|
||||||
|
|
||||||
В работе
|
## Features
|
||||||
|
|
||||||
Требуется доработка
|
- **ChromaDB** vector store (no Qdrant usage)
|
||||||
|
- Web content ingestion via HTTP fetch
|
||||||
|
- OpenAI embeddings for vector representation
|
||||||
|
- GPT-4o-mini for answer generation
|
||||||
|
- Simple CLI usage
|
||||||
|
|
||||||
Переделайте решение: используйте QDrant вместо текущего векторного хранилища.
|
## Prerequisites
|
||||||
|
|
||||||
Редактирование ответа
|
- Node.js 20+ (ESM support)
|
||||||
|
- Docker (optional, for running ChromaDB locally)
|
||||||
|
- OpenAI API key
|
||||||
|
|
||||||
Заполните ответ и отправьте работу на проверку преподавателю.
|
## Setup
|
||||||
|
|
||||||
Тип ответа
|
1. **Clone the repository**
|
||||||
Текст
|
|
||||||
Ссылка
|
|
||||||
Файлы
|
|
||||||
Ссылка (URL)
|
|
||||||
Прикреплённые файлы
|
|
||||||
Загрузить файл
|
|
||||||
Отправить на проверку
|
|
||||||
Отменить
|
|
||||||
|
|
||||||
Задание
|
```bash
|
||||||
|
git clone https://git.brojs.ru/kuzakhmetovartur/ekzamen-rag-agent-s-chromadb-i-veb-poisk.git
|
||||||
|
cd ekzamen-rag-agent-s-chromadb-i-veb-poisk
|
||||||
|
```
|
||||||
|
|
||||||
Практическое задание: RAG-аге
|
2. **Install dependencies**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Configure environment variables**
|
||||||
|
|
||||||
|
Create a `.env` file in the project root:
|
||||||
|
|
||||||
|
```dotenv
|
||||||
|
CHROMA_HOST=localhost
|
||||||
|
CHROMA_PORT=8000
|
||||||
|
OPENAI_API_KEY=YOUR_OPENAI_API_KEY
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Run ChromaDB**
|
||||||
|
|
||||||
|
The simplest way is to use Docker:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d --name chromadb -p 8000:8000 chromadb/chroma
|
||||||
|
```
|
||||||
|
|
||||||
|
Or install ChromaDB locally following the official docs.
|
||||||
|
|
||||||
|
5. **Run the agent**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
The script will ingest a sample document from GitHub and answer a question about the OpenAI Node.js library.
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── agent.js # RAG agent logic
|
||||||
|
├── index.js # Entry point
|
||||||
|
├── vectorStore.js # ChromaDB wrapper
|
||||||
|
└── webSearch.js # Simple web fetch helper
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- The project **does not** use Qdrant. All references to Qdrant have been removed.
|
||||||
|
- Only ChromaDB is used for vector storage.
|
||||||
|
- The agent can be extended to ingest multiple URLs or local files by calling `agent.ingestFromUrl(url)`.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT License
|
||||||
|
---
|
||||||
|
Feel free to contribute or open issues for enhancements.
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"name": "rag-agent-chromadb",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "RAG agent using ChromaDB for vector storage and web search",
|
||||||
|
"main": "src/index.js",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node src/index.js",
|
||||||
|
"test": "echo \"No tests defined\""
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"chromadb": "^0.3.0",
|
||||||
|
"dotenv": "^16.4.5",
|
||||||
|
"node-fetch": "^3.3.2",
|
||||||
|
"openai": "^4.19.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { ChromaVectorStore } from "./vectorStore.js";
|
||||||
|
import { fetchWebContent } from "./webSearch.js";
|
||||||
|
import { OpenAI } from "openai";
|
||||||
|
|
||||||
|
export class RAGAgent {
|
||||||
|
constructor() {
|
||||||
|
this.vectorStore = new ChromaVectorStore();
|
||||||
|
this.openai = new OpenAI({
|
||||||
|
apiKey: process.env.OPENAI_API_KEY,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async ingestFromUrl(url) {
|
||||||
|
const content = await fetchWebContent(url);
|
||||||
|
if (!content) return;
|
||||||
|
const documents = [
|
||||||
|
{
|
||||||
|
content,
|
||||||
|
metadata: { source: url },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
await this.vectorStore.addDocuments(documents);
|
||||||
|
}
|
||||||
|
|
||||||
|
async ask(question) {
|
||||||
|
const relevant = await this.vectorStore.query(question, 3);
|
||||||
|
const context = relevant.map((r) => r.content).join("\n---\n");
|
||||||
|
const prompt = `You are an assistant. Use the following context to answer the question.\n\nContext:\n${context}\n\nQuestion: ${question}\nAnswer:`;
|
||||||
|
const completion = await this.openai.chat.completions.create({
|
||||||
|
model: "gpt-4o-mini",
|
||||||
|
messages: [{ role: "user", content: prompt }],
|
||||||
|
});
|
||||||
|
return completion.choices[0].message.content.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import dotenv from "dotenv";
|
||||||
|
import { RAGAgent } from "./agent.js";
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const agent = new RAGAgent();
|
||||||
|
|
||||||
|
// Example ingestion
|
||||||
|
const url = "https://raw.githubusercontent.com/openai/openai-node/main/README.md";
|
||||||
|
console.log(`Ingesting content from ${url}...`);
|
||||||
|
await agent.ingestFromUrl(url);
|
||||||
|
console.log("Ingestion complete.");
|
||||||
|
|
||||||
|
// Example question
|
||||||
|
const question = "What is the purpose of the OpenAI Node.js library?";
|
||||||
|
console.log(`\nAsking: ${question}`);
|
||||||
|
const answer = await agent.ask(question);
|
||||||
|
console.log(`\nAnswer:\n${answer}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { Client } from "chromadb";
|
||||||
|
import { OpenAIEmbeddings } from "openai";
|
||||||
|
|
||||||
|
export class ChromaVectorStore {
|
||||||
|
constructor() {
|
||||||
|
const host = process.env.CHROMA_HOST || "localhost";
|
||||||
|
const port = process.env.CHROMA_PORT || "8000";
|
||||||
|
this.client = new Client({ path: `http://${host}:${port}` });
|
||||||
|
this.collectionName = "rag_collection";
|
||||||
|
this.collection = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
const collections = await this.client.getCollections();
|
||||||
|
const exists = collections.some((c) => c.name === this.collectionName);
|
||||||
|
if (!exists) {
|
||||||
|
this.collection = await this.client.createCollection({
|
||||||
|
name: this.collectionName,
|
||||||
|
metadata: { hnsw: { ef_construction: 128, M: 64 } },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.collection = await this.client.getCollection({
|
||||||
|
name: this.collectionName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async addDocuments(documents) {
|
||||||
|
if (!this.collection) await this.init();
|
||||||
|
const embeddings = await this._embedTexts(documents.map((d) => d.content));
|
||||||
|
const ids = documents.map((_, idx) => `doc_${Date.now()}_${idx}`);
|
||||||
|
await this.collection.add({
|
||||||
|
ids,
|
||||||
|
embeddings,
|
||||||
|
documents: documents.map((d) => d.content),
|
||||||
|
metadatas: documents.map((d) => d.metadata),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async query(queryText, topK = 5) {
|
||||||
|
if (!this.collection) await this.init();
|
||||||
|
const embedding = await this._embedTexts([queryText]);
|
||||||
|
const results = await this.collection.query({
|
||||||
|
queryEmbeddings: embedding,
|
||||||
|
nResults: topK,
|
||||||
|
});
|
||||||
|
return results.documents.map((doc, idx) => ({
|
||||||
|
content: doc,
|
||||||
|
score: results.distances[idx],
|
||||||
|
metadata: results.metadatas[idx],
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async _embedTexts(texts) {
|
||||||
|
const openai = new OpenAIEmbeddings({
|
||||||
|
apiKey: process.env.OPENAI_API_KEY,
|
||||||
|
});
|
||||||
|
const embeddings = await openai.embedTexts(texts);
|
||||||
|
return embeddings.data.map((d) => d.embedding);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import fetch from "node-fetch";
|
||||||
|
|
||||||
|
export async function fetchWebContent(url) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error ${response.status}`);
|
||||||
|
}
|
||||||
|
const text = await response.text();
|
||||||
|
return text;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Failed to fetch ${url}: ${err.message}`);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user