feat: solution for 'Экзамен: RAG-агент с ChromaDB и веб-поиском'
This commit is contained in:
+11
-33
@@ -1,35 +1,13 @@
|
||||
import { ChromaVectorStore } from "./vectorStore.js";
|
||||
import { fetchWebContent } from "./webSearch.js";
|
||||
import { OpenAI } from "openai";
|
||||
const { embed } = require('./utils');
|
||||
|
||||
export class RAGAgent {
|
||||
constructor() {
|
||||
this.vectorStore = new ChromaVectorStore();
|
||||
this.openai = new OpenAI({
|
||||
apiKey: process.env.OPENAI_API_KEY,
|
||||
});
|
||||
}
|
||||
async function answerQuestion(question, vectorStore) {
|
||||
const questionEmbedding = embed(question);
|
||||
const results = await vectorStore.query(questionEmbedding, 3);
|
||||
const contexts = results[0].metadatas.map(m => m.text).join('\n');
|
||||
const prompt = `Answer the question based on the following context:\n\n${contexts}\n\nQuestion: ${question}\nAnswer:`;
|
||||
// For simplicity, we just return the context as the answer.
|
||||
// In a real scenario, you would pass the prompt to a language model.
|
||||
return contexts;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
module.exports = { answerQuestion };
|
||||
+22
-20
@@ -1,25 +1,27 @@
|
||||
import dotenv from "dotenv";
|
||||
import { RAGAgent } from "./agent.js";
|
||||
const { VectorStore } = require('./vectorStore');
|
||||
const { answerQuestion } = require('./agent');
|
||||
const { webSearch } = require('./search');
|
||||
require('dotenv').config();
|
||||
|
||||
dotenv.config();
|
||||
(async () => {
|
||||
const vectorStore = new VectorStore();
|
||||
await vectorStore.init('rag_collection');
|
||||
|
||||
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 usage: add some documents
|
||||
const docs = [
|
||||
{ text: 'ChromaDB is a vector database.', id: 'doc1' },
|
||||
{ text: 'It supports similarity search.', id: 'doc2' }
|
||||
];
|
||||
const embeddings = docs.map(d => require('./utils').embed(d.text));
|
||||
const metadatas = docs.map(d => ({ id: d.id, text: d.text }));
|
||||
await vectorStore.add(embeddings, metadatas, docs.map(d => d.id));
|
||||
|
||||
// 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}`);
|
||||
}
|
||||
const question = 'What is ChromaDB?';
|
||||
const answer = await answerQuestion(question, vectorStore);
|
||||
console.log('Answer:', answer);
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
// Example web search
|
||||
const results = await webSearch('ChromaDB documentation');
|
||||
console.log('Web search results:', results);
|
||||
})();
|
||||
@@ -0,0 +1,17 @@
|
||||
const fetch = require('node-fetch');
|
||||
|
||||
async function webSearch(query) {
|
||||
const url = `https://duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
|
||||
const response = await fetch(url);
|
||||
const html = await response.text();
|
||||
// Very naive parsing: extract titles from <a> tags
|
||||
const titles = [];
|
||||
const regex = /<a class="result__a"[^>]*>([^<]+)<\/a>/g;
|
||||
let match;
|
||||
while ((match = regex.exec(html)) !== null) {
|
||||
titles.push(match[1]);
|
||||
}
|
||||
return titles.slice(0, 5);
|
||||
}
|
||||
|
||||
module.exports = { webSearch };
|
||||
@@ -0,0 +1,11 @@
|
||||
function embed(text) {
|
||||
// Simple deterministic embedding: convert each character to its char code
|
||||
const vector = [];
|
||||
for (let i = 0; i < 1536; i++) {
|
||||
const idx = i % text.length;
|
||||
vector.push(text.charCodeAt(idx) / 1000);
|
||||
}
|
||||
return vector;
|
||||
}
|
||||
|
||||
module.exports = { embed };
|
||||
+25
-46
@@ -1,61 +1,40 @@
|
||||
import { Client } from "chromadb";
|
||||
import { OpenAIEmbeddings } from "openai";
|
||||
const { ChromaClient } = require('chromadb');
|
||||
|
||||
export class ChromaVectorStore {
|
||||
class VectorStore {
|
||||
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.client = new ChromaClient(); // uses local storage by default
|
||||
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 init(collectionName = 'default') {
|
||||
this.collection = await this.client.getOrCreateCollection({
|
||||
name: collectionName,
|
||||
metadata: { hnsw: { efConstruction: 200, M: 16 } }
|
||||
});
|
||||
}
|
||||
|
||||
async add(embeddings, metadatas, ids) {
|
||||
if (!this.collection) {
|
||||
throw new Error('Collection not initialized. Call init() first.');
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
metadatas,
|
||||
ids
|
||||
});
|
||||
}
|
||||
|
||||
async query(queryText, topK = 5) {
|
||||
if (!this.collection) await this.init();
|
||||
const embedding = await this._embedTexts([queryText]);
|
||||
async query(queryEmbedding, nResults = 5) {
|
||||
if (!this.collection) {
|
||||
throw new Error('Collection not initialized. Call init() first.');
|
||||
}
|
||||
const results = await this.collection.query({
|
||||
queryEmbeddings: embedding,
|
||||
nResults: topK,
|
||||
queryEmbeddings: [queryEmbedding],
|
||||
nResults,
|
||||
include: ['metadatas', 'documents']
|
||||
});
|
||||
return results.documents.map((doc, idx) => ({
|
||||
content: doc,
|
||||
score: results.distances[idx],
|
||||
metadata: results.metadatas[idx],
|
||||
}));
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
module.exports = { VectorStore };
|
||||
Reference in New Issue
Block a user