This commit is contained in:
@@ -1,166 +1,74 @@
|
||||
# RAG Agent with Retrieval-Augmented Generation
|
||||
# Agent with RAG Memory
|
||||
|
||||
**Version:** 20
|
||||
**Author:** Artur Kuzakhmetov
|
||||
**Course:** Deep Agents Virtual File System
|
||||
**Deadline:** 31.08.2026
|
||||
This project implements a simple command‑line agent that uses **Ollama embeddings** for a Retrieval‑Augmented Generation (RAG) style knowledge base.
|
||||
The agent supports two main tools:
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
This repository implements an educational agent that uses Retrieval-Augmented Generation (RAG) to answer user queries.
|
||||
The agent:
|
||||
|
||||
1. **Embeds** a collection of text documents into a FAISS vector store using OpenAI embeddings.
|
||||
2. **Retrieves** the most relevant passages for a user query.
|
||||
3. **Generates** a response with OpenAI GPT‑4, conditioned on the retrieved context.
|
||||
|
||||
The agent is exposed via a FastAPI web service with a single `/ask` endpoint.
|
||||
|
||||
---
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
.
|
||||
├── data/ # Place your .txt documents here
|
||||
├── src/
|
||||
│ └── index.py # FastAPI app and RAG logic
|
||||
├── .env # (Optional) Environment variables
|
||||
├── README.md
|
||||
└── requirements.txt
|
||||
```
|
||||
|
||||
> **Note:** The `data/` directory is **not** committed to version control.
|
||||
> Add your own documents there before running the agent.
|
||||
|
||||
---
|
||||
- **`search_knowledge_base`** – find the most relevant documents for a query.
|
||||
- **`add_to_knowledge_base`** – add new content to the knowledge base.
|
||||
|
||||
## Setup
|
||||
|
||||
### 1. Clone the Repository
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://git.brojs.ru/kuzakhmetovartur/agent-s-rag-pamyatyu.git
|
||||
cd agent-s-rag-pamyatyu
|
||||
|
||||
# Install dependencies
|
||||
npm install
|
||||
```
|
||||
|
||||
### 2. Create a Virtual Environment
|
||||
> **Note**: The project uses the `ollama-embeddings` package.
|
||||
> Make sure you have an Ollama server running locally (default `http://localhost:11434`).
|
||||
> You can change the host or model via environment variables:
|
||||
|
||||
```bash
|
||||
python -m venv .venv
|
||||
source .venv/bin/activate # On Windows: .venv\Scripts\activate
|
||||
# Example .env file
|
||||
OLLAMA_HOST=http://localhost:11434
|
||||
OLLAMA_MODEL=all-minilm
|
||||
```
|
||||
|
||||
### 3. Install Dependencies
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
> `requirements.txt` contains:
|
||||
> ```text
|
||||
> fastapi
|
||||
> uvicorn
|
||||
> langchain
|
||||
> openai
|
||||
> faiss-cpu
|
||||
> python-dotenv
|
||||
> ```
|
||||
|
||||
### 4. Set Up OpenAI API Key
|
||||
|
||||
Create a file named `.env` in the project root:
|
||||
|
||||
```dotenv
|
||||
OPENAI_API_KEY=sk-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
||||
```
|
||||
|
||||
> **Security:** Do **not** commit the `.env` file to version control.
|
||||
> Add it to `.gitignore` if you have one.
|
||||
|
||||
### 5. Add Documents
|
||||
|
||||
Place any number of `.txt` files in the `data/` directory.
|
||||
Each file will be treated as a separate document.
|
||||
|
||||
---
|
||||
|
||||
## Running the Agent
|
||||
|
||||
```bash
|
||||
uvicorn src.index:app --reload
|
||||
npm start
|
||||
```
|
||||
|
||||
The API will be available at `http://127.0.0.1:8000`.
|
||||
You will see a prompt:
|
||||
|
||||
### Example Request
|
||||
|
||||
```bash
|
||||
curl -X POST "http://127.0.0.1:8000/ask" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"question":"What is the capital of France?"}'
|
||||
```
|
||||
Agent>
|
||||
```
|
||||
|
||||
**Response**
|
||||
### Commands
|
||||
|
||||
```json
|
||||
{
|
||||
"answer": "The capital of France is Paris.",
|
||||
"sources": ["data/geo_facts.txt"]
|
||||
}
|
||||
- `/search <query>` – Search the knowledge base for the most relevant documents.
|
||||
- `/add <content>` – Add new content to the knowledge base.
|
||||
- `/exit` – Exit the program.
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
Agent> /add The quick brown fox jumps over the lazy dog.
|
||||
Content added with id 3f1c2e4b-...
|
||||
|
||||
Agent> /search fox
|
||||
Searching for "fox"...
|
||||
Top results:
|
||||
1. [3f1c2e4b-...] (0.9123)
|
||||
The quick brown fox jumps over the lazy dog.
|
||||
```
|
||||
|
||||
---
|
||||
## Project Structure
|
||||
|
||||
## Architecture Details
|
||||
- `src/embeddings.js` – Wrapper around `ollama-embeddings`.
|
||||
- `src/tools/searchKnowledgeBase.js` – Implements the search tool.
|
||||
- `src/tools/addToKnowledgeBase.js` – Implements the add tool.
|
||||
- `src/index.js` – CLI entry point and agent logic.
|
||||
- `package.json` – Dependencies and scripts.
|
||||
|
||||
| Component | Purpose | Library |
|
||||
|-----------|---------|---------|
|
||||
| **Document Loader** | Reads `.txt` files from `data/` | `langchain.document_loaders.DirectoryLoader` |
|
||||
| **Embeddings** | Converts text to vectors | `langchain.embeddings.openai.OpenAIEmbeddings` |
|
||||
| **Vector Store** | Stores and queries vectors | `langchain.vectorstores.FAISS` |
|
||||
| **Retriever** | Finds top‑k relevant documents | FAISS retriever |
|
||||
| **LLM** | Generates answer | `langchain.llms.OpenAI` (GPT‑4) |
|
||||
| **Chain** | Combines retrieval and generation | `langchain.chains.RetrievalQA` |
|
||||
| **API** | Exposes the agent | `FastAPI` |
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
The repository includes a simple integration test in `tests/test_agent.py` (not shown here).
|
||||
Run tests with:
|
||||
|
||||
```bash
|
||||
pytest
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Compliance with Course Guidelines
|
||||
|
||||
- **Educational Agent Solution**: The agent follows the structure outlined in the Deep Agents lecture, using a clear separation between data ingestion, retrieval, and generation.
|
||||
- **RAG Memory**: Implemented via FAISS vector store and OpenAI embeddings.
|
||||
- **Python 3.11+**: All code is compatible with Python 3.11 and above.
|
||||
- **Individual Assignment**: All work is authored by a single developer (Artur Kuzakhmetov).
|
||||
- **Versioning**: The repository is tagged as `v20` and the README reflects version 20.
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
This project is released under the MIT License.
|
||||
Feel free to adapt and extend it for your own educational projects.
|
||||
|
||||
---
|
||||
|
||||
## Contact
|
||||
|
||||
For questions or feedback, contact:
|
||||
|
||||
- **Email:** artur.kuzakhmetov@example.com
|
||||
- **GitLab:** https://git.brojs.ru/kuzakhmetovartur/agent-s-rag-pamyatyu
|
||||
## Extending
|
||||
|
||||
The current implementation uses an in‑memory vector store.
|
||||
To persist data or use a more sophisticated vector database, replace the `knowledgeBase` array in `searchKnowledgeBase.js` with your preferred storage solution.
|
||||
|
||||
---
|
||||
+67
-59
@@ -1,72 +1,80 @@
|
||||
**What was implemented**
|
||||
- A FastAPI service exposing a single `/ask` endpoint that accepts a user question and returns an answer together with the sources used.
|
||||
- RAG (Retrieval‑Augmented Generation) logic built with LangChain: documents from `data/` are embedded with OpenAI embeddings, stored in a FAISS vector store, and queried by a `RetrievalQA` chain that feeds the retrieved passages to GPT‑4.
|
||||
- Automatic startup loading of documents, vector store creation, and agent construction so the API is ready to serve immediately after launch.
|
||||
**SOLUTION.md**
|
||||
|
||||
**Why the main parts satisfy the assignment**
|
||||
- **RAG memory**: `create_vectorstore` builds a FAISS index from the loaded documents, and `build_agent` wires this index into a `RetrievalQA` chain that retrieves relevant passages before generation.
|
||||
- **Course guidelines**: The solution follows the Deep Agents Virtual File System pattern – a single `src/index.py` module, clear separation of concerns (loading, vector store, agent, API), and use of environment variables for secrets.
|
||||
- **Python implementation**: All code is pure Python 3.11+, uses only standard libraries and well‑documented third‑party packages (`fastapi`, `langchain`, `openai`, `dotenv`).
|
||||
- **Individual assignment**: No shared state or external services beyond the OpenAI API; the repository contains only the student’s code.
|
||||
---
|
||||
|
||||
**Key code excerpts**
|
||||
### Что реализовано
|
||||
1. **Инструменты RAG**
|
||||
* `search_knowledge_base(query, topK)` – ищет наиболее релевантные документы в памяти.
|
||||
* `add_to_knowledge_base(content)` – добавляет новый контент в память.
|
||||
|
||||
*Loading documents* (`src/index.py`)
|
||||
```python
|
||||
def load_documents(path: Path) -> List:
|
||||
if not path.exists() or not path.is_dir():
|
||||
print(f"Warning: Data directory '{path}' not found. No documents loaded.")
|
||||
return []
|
||||
2. **Стек эмбеддингов**
|
||||
* Заменён `OpenAIEmbeddings` на `OllamaEmbeddings`.
|
||||
* В `package.json` добавлена зависимость `ollama-embeddings`.
|
||||
|
||||
loader = DirectoryLoader(str(path), glob="**/*.txt")
|
||||
documents = loader.load()
|
||||
print(f"Loaded {len(documents)} documents from '{path}'.")
|
||||
return documents
|
||||
3. **Интеграция**
|
||||
* Инструменты подключены в `src/index.js` и доступны через CLI‑команды `/search` и `/add`.
|
||||
* Все операции с эмбеддингами используют экземпляр `OllamaEmbeddings` из `src/embeddings.js`.
|
||||
|
||||
---
|
||||
|
||||
### Почему это соответствует требованиям
|
||||
* **Наличие инструментов** – файлы `searchKnowledgeBase.js` и `addToKnowledgeBase.js` экспортируют требуемые функции, которые можно вызывать из любого модуля.
|
||||
* **Использование OllamaEmbeddings** – в `embeddings.js` создаётся единственный экземпляр `OllamaEmbeddings`, а в инструментах вызывается `embeddings.embedQuery`.
|
||||
* **Обновлённые импорты** – все модули импортируют `embeddings` из `src/embeddings.js`, а не из OpenAI.
|
||||
* **Пакетная зависимость** – `ollama-embeddings` присутствует в `package.json`, что позволяет npm установить нужный пакет.
|
||||
|
||||
---
|
||||
|
||||
### Ключевые фрагменты кода
|
||||
|
||||
**src/embeddings.js** – инициализация OllamaEmbeddings
|
||||
```js
|
||||
import { OllamaEmbeddings } from 'ollama-embeddings';
|
||||
const modelName = process.env.OLLAMA_MODEL || 'all-minilm';
|
||||
export const embeddings = new OllamaEmbeddings({
|
||||
model: modelName,
|
||||
host: process.env.OLLAMA_HOST || 'http://localhost:11434'
|
||||
});
|
||||
```
|
||||
|
||||
*Creating the vector store* (`src/index.py`)
|
||||
```python
|
||||
def create_vectorstore(documents: List) -> FAISS:
|
||||
embeddings = OpenAIEmbeddings()
|
||||
vectorstore = FAISS.from_documents(documents, embeddings)
|
||||
print("FAISS vector store created.")
|
||||
return vectorstore
|
||||
**src/tools/searchKnowledgeBase.js** – поиск по памяти
|
||||
```js
|
||||
export async function search_knowledge_base(query, topK = 3) {
|
||||
const queryEmbedding = await embeddings.embedQuery(query);
|
||||
const scored = knowledgeBase.map(entry => ({
|
||||
id: entry.id,
|
||||
content: entry.content,
|
||||
score: cosineSimilarity(queryEmbedding, entry.embedding)
|
||||
}));
|
||||
scored.sort((a, b) => b.score - a.score);
|
||||
return scored.slice(0, topK);
|
||||
}
|
||||
```
|
||||
|
||||
*Building the RetrievalQA agent* (`src/index.py`)
|
||||
```python
|
||||
def build_agent(vectorstore: FAISS) -> RetrievalQA:
|
||||
llm = OpenAI(model_name="gpt-4", temperature=0, openai_api_key=OPENAI_API_KEY)
|
||||
retriever = vectorstore.as_retriever(search_kwargs={"k": 4})
|
||||
qa_chain = RetrievalQA.from_chain_type(
|
||||
llm=llm,
|
||||
chain_type="stuff",
|
||||
retriever=retriever,
|
||||
return_source_documents=True,
|
||||
)
|
||||
print("RetrievalQA agent constructed.")
|
||||
return qa_chain
|
||||
**src/tools/addToKnowledgeBase.js** – добавление контента
|
||||
```js
|
||||
export async function add_to_knowledge_base(content) {
|
||||
const embedding = await embeddings.embedQuery(content);
|
||||
const id = uuidv4();
|
||||
knowledgeBase.push({ id, content, embedding });
|
||||
return { id };
|
||||
}
|
||||
```
|
||||
|
||||
*FastAPI endpoint* (`src/index.py`)
|
||||
```python
|
||||
@app.post("/ask", response_model=AnswerResponse)
|
||||
def ask_question(request: QuestionRequest):
|
||||
if not agent:
|
||||
raise HTTPException(status_code=500, detail="Agent not initialized.")
|
||||
try:
|
||||
result = agent({"question": request.question})
|
||||
answer = result.get("answer", "")
|
||||
sources = [doc.metadata.get("source", "") for doc in result.get("source_documents", [])]
|
||||
return AnswerResponse(answer=answer, sources=sources)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
**src/index.js** – CLI‑интеграция инструментов
|
||||
```js
|
||||
import { search_knowledge_base } from './tools/searchKnowledgeBase.js';
|
||||
import { add_to_knowledge_base } from './tools/addToKnowledgeBase.js';
|
||||
...
|
||||
if (trimmed.startsWith('/search ')) { … }
|
||||
else if (trimmed.startsWith('/add ')) { … }
|
||||
```
|
||||
|
||||
**Honest limitations**
|
||||
- The vector store is rebuilt on every server restart; no persistence across restarts.
|
||||
- No caching of embeddings or query results, which may increase latency for repeated queries.
|
||||
- Error handling is minimal – any exception during a request returns a generic 500 error.
|
||||
- The solution assumes all documents are plain `.txt`; other formats would need additional loaders.
|
||||
---
|
||||
|
||||
These points are acceptable for the current assignment scope and can be refined in future iterations.
|
||||
### Ограничения
|
||||
* Память реализована как простая массив‑объект, поэтому данные не сохраняются между перезапусками.
|
||||
* Нет обработки ошибок при работе с Ollama (например, недоступность сервера).
|
||||
* Для больших наборов данных поиск выполняется линейно; в продакшене стоит использовать индексирование.
|
||||
|
||||
---
|
||||
+5
-5
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"name": "agent-s-rag-pamyatyu",
|
||||
"version": "1.0.0",
|
||||
"description": "RAG agent using ChromaDB as the vector store",
|
||||
"description": "Agent with RAG memory using Ollama embeddings",
|
||||
"main": "src/index.js",
|
||||
"type": "commonjs",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node src/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"chromadb": "^0.3.0",
|
||||
"openai": "^3.3.0",
|
||||
"dotenv": "^16.0.0"
|
||||
"ollama-embeddings": "^1.0.0",
|
||||
"dotenv": "^16.4.5",
|
||||
"node-fetch": "^3.3.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { OllamaEmbeddings } from 'ollama-embeddings';
|
||||
|
||||
/**
|
||||
* Singleton instance of OllamaEmbeddings.
|
||||
* The model name can be overridden via the OLLAMA_MODEL environment variable.
|
||||
*/
|
||||
const modelName = process.env.OLLAMA_MODEL || 'all-minilm';
|
||||
export const embeddings = new OllamaEmbeddings({
|
||||
model: modelName,
|
||||
// Optional: specify the Ollama host if not default
|
||||
host: process.env.OLLAMA_HOST || 'http://localhost:11434'
|
||||
});
|
||||
|
||||
/**
|
||||
* Utility to embed a single string.
|
||||
* @param {string} text
|
||||
* @returns {Promise<number[]>} embedding vector
|
||||
*/
|
||||
export async function embedText(text) {
|
||||
return await embeddings.embedQuery(text);
|
||||
}
|
||||
+66
-2
@@ -1,3 +1,67 @@
|
||||
const Agent = require('./agent');
|
||||
import dotenv from 'dotenv';
|
||||
import readline from 'readline';
|
||||
import { search_knowledge_base } from './tools/searchKnowledgeBase.js';
|
||||
import { add_to_knowledge_base } from './tools/addToKnowledgeBase.js';
|
||||
|
||||
module.exports = { Agent };
|
||||
dotenv.config();
|
||||
|
||||
/**
|
||||
* Simple command-line agent that supports two commands:
|
||||
* 1. /search <query> - searches the knowledge base
|
||||
* 2. /add <content> - adds content to the knowledge base
|
||||
* Any other input is treated as a normal message and the agent echoes it back.
|
||||
*/
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
prompt: 'Agent> '
|
||||
});
|
||||
|
||||
console.log('Agent with RAG memory using Ollama embeddings.');
|
||||
console.log('Commands:');
|
||||
console.log(' /search <query> - Search knowledge base');
|
||||
console.log(' /add <content> - Add content to knowledge base');
|
||||
console.log(' /exit - Exit');
|
||||
rl.prompt();
|
||||
|
||||
rl.on('line', async (line) => {
|
||||
const trimmed = line.trim();
|
||||
if (trimmed === '/exit') {
|
||||
rl.close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (trimmed.startsWith('/search ')) {
|
||||
const query = trimmed.slice(8).trim();
|
||||
if (!query) {
|
||||
console.log('Please provide a query.');
|
||||
} else {
|
||||
console.log(`Searching for "${query}"...`);
|
||||
const results = await search_knowledge_base(query);
|
||||
if (results.length === 0) {
|
||||
console.log('No relevant documents found.');
|
||||
} else {
|
||||
console.log('Top results:');
|
||||
results.forEach((res, idx) => {
|
||||
console.log(`${idx + 1}. [${res.id}] (${res.score.toFixed(4)})`);
|
||||
console.log(` ${res.content}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (trimmed.startsWith('/add ')) {
|
||||
const content = trimmed.slice(5).trim();
|
||||
if (!content) {
|
||||
console.log('Please provide content to add.');
|
||||
} else {
|
||||
const { id } = await add_to_knowledge_base(content);
|
||||
console.log(`Content added with id ${id}.`);
|
||||
}
|
||||
} else {
|
||||
// Echo back the message (placeholder for more complex agent logic)
|
||||
console.log(`You said: ${trimmed}`);
|
||||
}
|
||||
rl.prompt();
|
||||
}).on('close', () => {
|
||||
console.log('Goodbye!');
|
||||
process.exit(0);
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import { embeddings } from '../embeddings.js';
|
||||
import { knowledgeBase } from './searchKnowledgeBase.js';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
/**
|
||||
* Add new content to the knowledge base.
|
||||
* @param {string} content
|
||||
* @returns {Promise<{id: string}>}
|
||||
*/
|
||||
export async function add_to_knowledge_base(content) {
|
||||
const embedding = await embeddings.embedQuery(content);
|
||||
const id = uuidv4();
|
||||
knowledgeBase.push({ id, content, embedding });
|
||||
return { id };
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { embeddings } from '../embeddings.js';
|
||||
|
||||
/**
|
||||
* In-memory knowledge base.
|
||||
* Each entry: { id, content, embedding }
|
||||
*/
|
||||
const knowledgeBase = [];
|
||||
|
||||
/**
|
||||
* Compute cosine similarity between two vectors.
|
||||
* @param {number[]} a
|
||||
* @param {number[]} b
|
||||
* @returns {number}
|
||||
*/
|
||||
function cosineSimilarity(a, b) {
|
||||
const dot = a.reduce((sum, ai, i) => sum + ai * b[i], 0);
|
||||
const normA = Math.sqrt(a.reduce((sum, ai) => sum + ai * ai, 0));
|
||||
const normB = Math.sqrt(b.reduce((sum, bi) => sum + bi * bi, 0));
|
||||
return dot / (normA * normB);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search the knowledge base for the most relevant documents.
|
||||
* @param {string} query
|
||||
* @param {number} topK
|
||||
* @returns {Promise<Array<{id: string, content: string, score: number}>>}
|
||||
*/
|
||||
export async function search_knowledge_base(query, topK = 3) {
|
||||
const queryEmbedding = await embeddings.embedQuery(query);
|
||||
const scored = knowledgeBase.map(entry => ({
|
||||
id: entry.id,
|
||||
content: entry.content,
|
||||
score: cosineSimilarity(queryEmbedding, entry.embedding)
|
||||
}));
|
||||
scored.sort((a, b) => b.score - a.score);
|
||||
return scored.slice(0, topK);
|
||||
}
|
||||
|
||||
/**
|
||||
* Expose the knowledge base for other modules (e.g., add tool).
|
||||
*/
|
||||
export { knowledgeBase };
|
||||
Reference in New Issue
Block a user