diff --git a/README.md b/README.md index a682c96..fc9a2f2 100644 --- a/README.md +++ b/README.md @@ -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 ` – Search the knowledge base for the most relevant documents. +- `/add ` – 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` | +## Extending ---- - -## 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 +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. --- \ No newline at end of file diff --git a/SOLUTION.md b/SOLUTION.md index d924a2a..d55b2df 100644 --- a/SOLUTION.md +++ b/SOLUTION.md @@ -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. \ No newline at end of file +### Ограничения +* Память реализована как простая массив‑объект, поэтому данные не сохраняются между перезапусками. +* Нет обработки ошибок при работе с Ollama (например, недоступность сервера). +* Для больших наборов данных поиск выполняется линейно; в продакшене стоит использовать индексирование. + +--- \ No newline at end of file diff --git a/package.json b/package.json index eee34a0..f0490ce 100644 --- a/package.json +++ b/package.json @@ -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" } } \ No newline at end of file diff --git a/src/embeddings.js b/src/embeddings.js new file mode 100644 index 0000000..14719d4 --- /dev/null +++ b/src/embeddings.js @@ -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} embedding vector + */ +export async function embedText(text) { + return await embeddings.embedQuery(text); +} \ No newline at end of file diff --git a/src/index.js b/src/index.js index 80d7af4..bad51f3 100644 --- a/src/index.js +++ b/src/index.js @@ -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 }; \ No newline at end of file +dotenv.config(); + +/** + * Simple command-line agent that supports two commands: + * 1. /search - searches the knowledge base + * 2. /add - 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 - Search knowledge base'); +console.log(' /add - 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); +}); \ No newline at end of file diff --git a/src/tools/addToKnowledgeBase.js b/src/tools/addToKnowledgeBase.js new file mode 100644 index 0000000..c726f6f --- /dev/null +++ b/src/tools/addToKnowledgeBase.js @@ -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 }; +} \ No newline at end of file diff --git a/src/tools/searchKnowledgeBase.js b/src/tools/searchKnowledgeBase.js new file mode 100644 index 0000000..2b86044 --- /dev/null +++ b/src/tools/searchKnowledgeBase.js @@ -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>} + */ +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 }; \ No newline at end of file