feat: solution for 'Агент с RAG-памятью'
CI / build (push) Has been cancelled

This commit is contained in:
2026-07-01 13:08:56 +03:00
parent 6da212bf32
commit 6022a43714
7 changed files with 260 additions and 202 deletions
+44 -136
View File
@@ -1,166 +1,74 @@
# RAG Agent with Retrieval-Augmented Generation # Agent with RAG Memory
**Version:** 20 This project implements a simple commandline agent that uses **Ollama embeddings** for a RetrievalAugmented Generation (RAG) style knowledge base.
**Author:** Artur Kuzakhmetov The agent supports two main tools:
**Course:** Deep Agents Virtual File System
**Deadline:** 31.08.2026
--- - **`search_knowledge_base`** find the most relevant documents for a query.
- **`add_to_knowledge_base`** add new content to the knowledge base.
## 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 GPT4, 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.
---
## Setup ## Setup
### 1. Clone the Repository
```bash ```bash
# Clone the repository
git clone https://git.brojs.ru/kuzakhmetovartur/agent-s-rag-pamyatyu.git git clone https://git.brojs.ru/kuzakhmetovartur/agent-s-rag-pamyatyu.git
cd agent-s-rag-pamyatyu 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 ```bash
python -m venv .venv # Example .env file
source .venv/bin/activate # On Windows: .venv\Scripts\activate 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 ## Running the Agent
```bash ```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 ```
Agent>
```bash
curl -X POST "http://127.0.0.1:8000/ask" \
-H "Content-Type: application/json" \
-d '{"question":"What is the capital of France?"}'
``` ```
**Response** ### Commands
```json - `/search <query>` Search the knowledge base for the most relevant documents.
{ - `/add <content>` Add new content to the knowledge base.
"answer": "The capital of France is Paris.", - `/exit` Exit the program.
"sources": ["data/geo_facts.txt"]
} 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 | ## Extending
|-----------|---------|---------|
| **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 topk relevant documents | FAISS retriever |
| **LLM** | Generates answer | `langchain.llms.OpenAI` (GPT4) |
| **Chain** | Combines retrieval and generation | `langchain.chains.RetrievalQA` |
| **API** | Exposes the agent | `FastAPI` |
--- The current implementation uses an inmemory vector store.
To persist data or use a more sophisticated vector database, replace the `knowledgeBase` array in `searchKnowledgeBase.js` with your preferred storage solution.
## 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
--- ---
+67 -59
View File
@@ -1,72 +1,80 @@
**What was implemented** **SOLUTION.md**
- A FastAPI service exposing a single `/ask` endpoint that accepts a user question and returns an answer together with the sources used.
- RAG (RetrievalAugmented 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 GPT4.
- Automatic startup loading of documents, vector store creation, and agent construction so the API is ready to serve immediately after launch.
**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 welldocumented thirdparty packages (`fastapi`, `langchain`, `openai`, `dotenv`).
- **Individual assignment**: No shared state or external services beyond the OpenAI API; the repository contains only the students code.
**Key code excerpts** ### Что реализовано
1. **Инструменты RAG**
* `search_knowledge_base(query, topK)` – ищет наиболее релевантные документы в памяти.
* `add_to_knowledge_base(content)` – добавляет новый контент в память.
*Loading documents* (`src/index.py`) 2. **Стек эмбеддингов**
```python * Заменён `OpenAIEmbeddings` на `OllamaEmbeddings`.
def load_documents(path: Path) -> List: * В `package.json` добавлена зависимость `ollama-embeddings`.
if not path.exists() or not path.is_dir():
print(f"Warning: Data directory '{path}' not found. No documents loaded.")
return []
loader = DirectoryLoader(str(path), glob="**/*.txt") 3. **Интеграция**
documents = loader.load() * Инструменты подключены в `src/index.js` и доступны через CLI‑команды `/search` и `/add`.
print(f"Loaded {len(documents)} documents from '{path}'.") * Все операции с эмбеддингами используют экземпляр `OllamaEmbeddings` из `src/embeddings.js`.
return documents
---
### Почему это соответствует требованиям
* **Наличие инструментов** – файлы `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`) **src/tools/searchKnowledgeBase.js** – поиск по памяти
```python ```js
def create_vectorstore(documents: List) -> FAISS: export async function search_knowledge_base(query, topK = 3) {
embeddings = OpenAIEmbeddings() const queryEmbedding = await embeddings.embedQuery(query);
vectorstore = FAISS.from_documents(documents, embeddings) const scored = knowledgeBase.map(entry => ({
print("FAISS vector store created.") id: entry.id,
return vectorstore 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`) **src/tools/addToKnowledgeBase.js** – добавление контента
```python ```js
def build_agent(vectorstore: FAISS) -> RetrievalQA: export async function add_to_knowledge_base(content) {
llm = OpenAI(model_name="gpt-4", temperature=0, openai_api_key=OPENAI_API_KEY) const embedding = await embeddings.embedQuery(content);
retriever = vectorstore.as_retriever(search_kwargs={"k": 4}) const id = uuidv4();
qa_chain = RetrievalQA.from_chain_type( knowledgeBase.push({ id, content, embedding });
llm=llm, return { id };
chain_type="stuff", }
retriever=retriever,
return_source_documents=True,
)
print("RetrievalQA agent constructed.")
return qa_chain
``` ```
*FastAPI endpoint* (`src/index.py`) **src/index.js** – CLI‑интеграция инструментов
```python ```js
@app.post("/ask", response_model=AnswerResponse) import { search_knowledge_base } from './tools/searchKnowledgeBase.js';
def ask_question(request: QuestionRequest): import { add_to_knowledge_base } from './tools/addToKnowledgeBase.js';
if not agent: ...
raise HTTPException(status_code=500, detail="Agent not initialized.") if (trimmed.startsWith('/search ')) { }
try: else if (trimmed.startsWith('/add ')) { }
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))
``` ```
**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
View File
@@ -1,15 +1,15 @@
{ {
"name": "agent-s-rag-pamyatyu", "name": "agent-s-rag-pamyatyu",
"version": "1.0.0", "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", "main": "src/index.js",
"type": "commonjs", "type": "module",
"scripts": { "scripts": {
"start": "node src/index.js" "start": "node src/index.js"
}, },
"dependencies": { "dependencies": {
"chromadb": "^0.3.0", "ollama-embeddings": "^1.0.0",
"openai": "^3.3.0", "dotenv": "^16.4.5",
"dotenv": "^16.0.0" "node-fetch": "^3.3.2"
} }
} }
+21
View File
@@ -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
View File
@@ -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);
});
+15
View File
@@ -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 };
}
+42
View File
@@ -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 };