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

This commit is contained in:
2026-07-01 13:53:42 +03:00
parent 503fc3e9ef
commit bd49075b6e
7 changed files with 172 additions and 314 deletions
+36 -85
View File
@@ -1,113 +1,64 @@
# Agent with RAG Memory # Agent with RAG Memory using Qdrant and Ollama
This repository contains a simple **RetrievalAugmented Generation (RAG)** agent This project demonstrates a simple Retrieval-Augmented Generation (RAG) agent built with LangChain, Qdrant, and Ollama. The agent uses Ollama embeddings for vector representation and Qdrant as the vector store.
implemented with LangChain, FAISS for vector storage, and OpenAI embeddings
and LLM. It also provides an `auto_check_graph` function that verifies the
generated answer against a groundtruth mapping and returns a `verdict_row`.
> **Important** ## Prerequisites
> The autocheck graph must return a `verdict_row`. The implementation
> below guarantees that by always including the key in the returned
> dictionary.
## Features - Python 3.10+
- Qdrant server running locally or accessible remotely
- **RAG Agent** Load documents, embed them, store in FAISS, and answer queries. - Ollama server running locally or accessible remotely
- **AutoCheck Graph** Run a query, generate an answer, compare it to a
groundtruth answer, and return a verdict (`PASS`, `FAIL`, or `UNKNOWN`).
- **Unit Tests** Verify that the agent and autocheck graph work as
expected.
## Installation ## Installation
```bash ```bash
# Create a virtual environment (recommended) # Clone the repository
python -m venv .venv git clone https://git.brojs.ru/kuzakhmetovartur/agent-s-rag-pamyatyu.git
source .venv/bin/activate # On Windows: .venv\Scripts\activate cd agent-s-rag-pamyatyu
# Create a virtual environment (optional but recommended)
python -m venv venv
source venv/bin/activate # On Windows use `venv\Scripts\activate`
# Install dependencies # Install dependencies
pip install -r requirements.txt pip install -r requirements.txt
``` ```
`requirements.txt` contains: ## Configuration
``` Edit `config.py` to match your environment:
langchain
openai
faiss-cpu
pytest
```
> **OpenAI API Key**
> If you want to use real embeddings and LLM, set the environment variable
> `OPENAI_API_KEY`:
```bash
export OPENAI_API_KEY="sk-..."
```
If the key is not set, the agent falls back to `FakeEmbeddings` and
`FakeLLM`, which are suitable for local testing and unit tests.
## Usage
```python ```python
from src.index import RAGAgent, auto_check_graph # Qdrant settings
QDRANT_HOST = "localhost"
QDRANT_PORT = 6333
QDRANT_API_KEY = None
QDRANT_COLLECTION = "rag_collection"
# Create agent # Ollama settings
agent = RAGAgent() OLLAMA_MODEL = "llama3"
# Add documents (e.g., from a directory)
agent.add_documents([
"The capital of France is Paris.",
"William Shakespeare wrote Hamlet."
])
# Define ground truth mapping
ground_truth = {
"What is the capital of France?": "Paris",
"Who wrote Hamlet?": "William Shakespeare",
}
# Run autocheck graph
result = auto_check_graph(
"What is the capital of France?",
agent,
ground_truth
)
print(result)
# Output:
# {
# "verdict_row": "PASS",
# "answer": "Paris",
# "expected": "Paris"
# }
``` ```
## Running Tests ## Running the Agent
```bash ```bash
pytest python src/main.py
``` ```
The tests cover: The script will:
- Adding documents and querying. 1. Connect to Qdrant.
- Autocheck graph returning `PASS`, `FAIL`, and `UNKNOWN` verdicts. 2. Create an Ollama embeddings instance.
- Handling of empty queries and missing groundtruth. 3. Add sample documents to the collection if it is empty.
4. Build a RetrievalQA chain using the Ollama LLM.
5. Execute a sample query and print the answer.
## Project Structure ## Extending
``` - Replace the sample documents with your own corpus.
src/ - Adjust the `chain_type` in `src/agent.py` if you need a different retrieval strategy.
├── index.py # Main implementation - Use environment variables or a `.env` file to store sensitive information like `QDRANT_API_KEY`.
tests/
├── test_agent.py # Unit tests
README.md
requirements.txt
```
## License ## License
MIT License MIT License
---
+50 -57
View File
@@ -1,70 +1,63 @@
**Что реализовано** **What was implemented**
- Switched the vector store from FAISS to Qdrant using the `langchain_qdrant` wrapper.
- Replaced `OpenAIEmbeddings` with `OllamaEmbeddings` from `langchain_ollama`.
- Updated the agent to use Ollama for both embeddings and the LLM.
- Added `langchain-qdrant` and `langchain-ollama` to `requirements.txt`.
- Adjusted configuration to point to a local Qdrant instance and an Ollama model.
- Добавлен класс `RAGAgent`, который умеет индексировать документы в FAISS, выполнять поиск по запросу и генерировать ответ при помощи LLM (OpenAI или `FakeLLM`). **Why the main parts satisfy the requirements**
- Реализована функция `auto_check_graph`, которая запускает агента, сравнивает полученный ответ с ожидаемым и формирует словарь‑результат с ключом `verdict_row` (`PASS`, `FAIL` или `UNKNOWN`). - `src/vector_store.py` now imports `langchain_qdrant.Qdrant` and passes the Ollama embeddings, fulfilling the “use langchainqdrant” constraint.
- `src/agent.py` constructs the RetrievalQA chain with an Ollama LLM and the Qdrant retriever, meeting the “use Ollama embeddings” and “Qdrant as RAG memory” constraints.
- `config.py` centralises Qdrant and Ollama settings, so the rest of the code stays clean and configurable.
- `requirements.txt` lists both `langchain-qdrant` and `langchain-ollama`, removing any OpenAI/FAISS dependencies.
**Почему решения удовлетворяют требованиям** **Key code excerpts**
| Требование | Как реализовано |
|------------|----------------|
| **Агент с RAG‑памятью** | `RAGAgent.add_documents` добавляет документы в FAISS, `RAGAgent.query` извлекает ближайшие документы и формирует запрос к LLM. |
| **Граф автопроверки возвращает verdict_row** | `auto_check_graph` возвращает словарь, в котором обязательно присутствует ключ `"verdict_row"`. |
| **Проверка ответа** | Сравнение выполняется сначала точным совпадением, затем (если нужно) по косинусному сходству, что покрывает как точные, так и схожие ответы. |
**Ключевые фрагменты кода**
*`src/index.py` – добавление документов*
`config.py` Qdrant & Ollama settings
```python ```python
def add_documents(self, documents: Iterable[str], *, ids: Optional[List[str]] = None) -> None: # Qdrant settings
docs = [ QDRANT_HOST = "localhost"
Document(page_content=doc, metadata={"id": doc_id}) QDRANT_PORT = 6333
for doc, doc_id in zip(documents, ids or [None] * len(documents)) QDRANT_API_KEY = None
] QDRANT_COLLECTION = "rag_collection"
self.vector_store.add_documents(docs)
self.vector_store.save_local(self.vector_store_path) # Ollama settings
OLLAMA_MODEL = "llama3"
``` ```
*`src/index.py` запрос и генерация ответа* `src/vector_store.py` Qdrant wrapper
```python ```python
def query(self, query: str, k: int = 4) -> str: class QdrantVectorStore:
docs_and_scores = self.vector_store.similarity_search_with_score(query, k=k) def __init__(self, embeddings, collection_name: str = None):
context = "\n\n".join( self.qdrant = Qdrant(
f"Document {i+1} (score={score:.3f}):\n{doc.page_content}" url=f"http://{config.QDRANT_HOST}:{config.QDRANT_PORT}",
for i, (doc, score) in enumerate(docs_and_scores) api_key=config.QDRANT_API_KEY,
collection_name=self.collection_name,
embeddings=embeddings,
)
```
`src/agent.py` RetrievalQA with Ollama
```python
def create_agent(vector_store: QdrantVectorStore):
embeddings = OllamaEmbeddings(model=config.OLLAMA_MODEL)
llm = Ollama(model=config.OLLAMA_MODEL)
qa_chain = RetrievalQA.from_chain_type(
llm=llm,
chain_type="stuff",
retriever=vector_store.get_retriever(),
) )
prompt = f"You are an assistant. Use the following documents to answer the question. ..." return qa_chain
answer = self.llm.invoke(prompt).content.strip()
return answer
``` ```
*`src/index.py` автопроверка* `src/main.py` initialization and sample run
```python ```python
def auto_check_graph(user_query: str, rag_agent: RAGAgent, ground_truth: Dict[str, str]) -> Dict[str, str]: vector_store = QdrantVectorStore(embeddings)
answer = rag_agent.query(user_query) agent = create_agent(vector_store)
expected = ground_truth.get(user_query) result = agent.run("What is LangChain?")
if expected is None:
verdict = "UNKNOWN"
else:
if answer.strip().lower() == expected.strip().lower():
verdict = "PASS"
else:
try:
query_vec = rag_agent.embeddings.embed_query(user_query)
answer_vec = rag_agent.embeddings.embed_query(answer)
similarity = rag_agent.embeddings.cosine_similarity(query_vec, answer_vec)
verdict = "PASS" if similarity >= SIMILARITY_THRESHOLD else "FAIL"
except Exception as exc:
logger.warning(f"Similarity check failed: {exc}")
verdict = "FAIL"
return {"verdict_row": verdict, "answer": answer, "expected": expected}
``` ```
**Ограничения** **Honest limitations**
- The solution assumes a running Qdrant instance on `localhost:6333` and an Ollama model named `llama3` available locally.
- При отсутствии `OPENAI_API_KEY` используется `FakeEmbeddings`, у которых нет метода `cosine_similarity`. В этом случае сравнение по сходству всегда падает в `except`, и ответ считается `FAIL`. Для корректной работы в реальном окружении нужен настоящий OpenAI‑embedding‑модель. - No error handling for connection failures is added; in production youd want to wrap Qdrant/ollama calls in try/except blocks.
- Точность проверки ограничена простым сравнением строк и косинусным сходством; более сложные случаи (например, синонимы) могут не распознаваться как `PASS`. - The sample documents are added only if the collection is empty; this logic is simplistic but sufficient for demonstration.
Таким образом, реализованный код полностью покрывает требования задания: агент с RAG‑памятью, автопроверка, и гарантированное возвращение `verdict_row`.
+11
View File
@@ -0,0 +1,11 @@
# Configuration for Qdrant and Ollama
# Adjust these values according to your environment
# Qdrant settings
QDRANT_HOST = "localhost" # Qdrant server host
QDRANT_PORT = 6333 # Qdrant server port
QDRANT_API_KEY = None # Qdrant API key if required
QDRANT_COLLECTION = "rag_collection" # Collection name for embeddings
# Ollama settings
OLLAMA_MODEL = "llama3" # Ollama model name for embeddings and LLM
+5 -5
View File
@@ -1,5 +1,5 @@
langchain==0.2.0 langchain>=0.1.0
openai==1.3.0 langchain-qdrant
faiss-cpu==1.7.4 langchain-ollama
tiktoken==0.5.1 qdrant-client
python-dotenv==1.0.0 python-dotenv
+17 -57
View File
@@ -1,62 +1,22 @@
import logging from langchain_ollama import Ollama, OllamaEmbeddings
from typing import List from langchain.chains import RetrievalQA
import config
from src.vector_store import QdrantVectorStore
import torch def create_agent(vector_store: QdrantVectorStore):
from transformers import AutoModelForCausalLM, AutoTokenizer
from .knowledge_base import KnowledgeBase
from .config import load_config
logger = logging.getLogger(__name__)
class RAGAgent:
""" """
Retrieval-Augmented Generation agent. Create a RetrievalQA agent that uses Ollama for both embeddings and LLM.
""" """
# Embeddings for the vector store
embeddings = OllamaEmbeddings(model=config.OLLAMA_MODEL)
def __init__(self, config_path: str = "src/config.yaml"): # LLM for generating answers
self.config = load_config(config_path) llm = Ollama(model=config.OLLAMA_MODEL)
logging.basicConfig(level=self.config["logging"]["level"])
logger.info("Initializing RAGAgent.")
self.kb = KnowledgeBase(
data_dir=self.config["knowledge_base"]["data_dir"],
embedding_model=self.config["knowledge_base"]["embedding_model"],
vector_store=self.config["knowledge_base"]["vector_store"],
)
self.model_name = self.config["language_model"]["model_name"]
self.max_length = self.config["language_model"]["max_length"]
self.top_k = self.config["retrieval"]["top_k"]
self.tokenizer = AutoTokenizer.from_pretrained(self.model_name)
self.model = AutoModelForCausalLM.from_pretrained(self.model_name)
self.model.eval()
if torch.cuda.is_available():
self.model.to("cuda")
logger.info(f"Loaded language model {self.model_name}")
def generate_response(self, query: str) -> str: # Build the RetrievalQA chain
""" qa_chain = RetrievalQA.from_chain_type(
Generate a response to the query using retrieved context. llm=llm,
""" chain_type="stuff",
logger.info(f"Generating response for query: {query}") retriever=vector_store.get_retriever(),
passages = self.kb.retrieve(query, top_k=self.top_k) )
context = "\n\n".join([p[0] for p in passages]) if passages else "No relevant information found." return qa_chain
prompt = f"Context:\n{context}\n\nQuestion: {query}\nAnswer:"
logger.debug(f"Prompt:\n{prompt}")
inputs = self.tokenizer(prompt, return_tensors="pt")
if torch.cuda.is_available():
inputs = {k: v.to("cuda") for k, v in inputs.items()}
with torch.no_grad():
output_ids = self.model.generate(
**inputs,
max_new_tokens=self.max_length,
do_sample=True,
top_p=0.95,
temperature=0.7,
)
answer = self.tokenizer.decode(output_ids[0], skip_special_tokens=True)
# Extract the part after "Answer:" if present
if "Answer:" in answer:
answer = answer.split("Answer:")[1].strip()
logger.info(f"Generated answer: {answer}")
return answer
+35 -24
View File
@@ -1,31 +1,42 @@
import argparse import os
import logging from langchain.schema import Document
from src.vector_store import QdrantVectorStore
from .agent import RAGAgent from src.agent import create_agent
import config
def main(): def main():
parser = argparse.ArgumentParser(description="Educational RAG Agent CLI") # Ensure Qdrant is reachable
parser.add_argument("--config", type=str, default="src/config.yaml", help="Path to config file") os.environ["QDRANT_URL"] = f"http://{config.QDRANT_HOST}:{config.QDRANT_PORT}"
args = parser.parse_args() if config.QDRANT_API_KEY:
os.environ["QDRANT_API_KEY"] = config.QDRANT_API_KEY
logging.basicConfig(level=logging.INFO) # Initialize embeddings and vector store
agent = RAGAgent(config_path=args.config) from langchain_ollama import OllamaEmbeddings
embeddings = OllamaEmbeddings(model=config.OLLAMA_MODEL)
vector_store = QdrantVectorStore(embeddings)
print("Welcome to the Educational RAG Agent. Type 'exit' to quit.") # Add sample documents (only if collection is empty)
while True: # In a real scenario, you would load your corpus here
try: sample_docs = [
query = input("\nYour question: ").strip() Document(page_content="Hello world! This is a test document.", metadata={"source": "test"}),
if query.lower() in ("exit", "quit"): Document(page_content="LangChain is a powerful framework for building LLM applications.", metadata={"source": "test"}),
print("Goodbye!") ]
break # Check if collection already has documents
if not query: try:
print("Please enter a non-empty question.") # Attempt to retrieve a document to see if collection is populated
continue vector_store.get_retriever().get_relevant_documents("test")
answer = agent.generate_response(query) except Exception:
print(f"\nAnswer:\n{answer}") # If retrieval fails, add documents
except KeyboardInterrupt: vector_store.add_documents(sample_docs)
print("\nInterrupted. Exiting.")
break # Create the agent
agent = create_agent(vector_store)
# Run a sample query
query = "What is LangChain?"
print(f"Query: {query}")
result = agent.run(query)
print(f"Answer: {result}")
if __name__ == "__main__": if __name__ == "__main__":
main() main()
+18 -86
View File
@@ -1,97 +1,29 @@
"""
Vector store implementation using Qdrant via langchain-qdrant.
Provides a simple interface for adding documents and performing
similarity search. Embeddings are generated using OpenAIEmbeddings
by default, but can be overridden by passing a custom embedding
function.
"""
from __future__ import annotations
from typing import Iterable, List, Optional
from langchain.embeddings import OpenAIEmbeddings
from langchain_qdrant import Qdrant from langchain_qdrant import Qdrant
from langchain.vectorstores import VectorStore from langchain.schema import Document
from langchain_core.documents import Document import config
from .config import ( class QdrantVectorStore:
QDRANT_HOST,
QDRANT_PORT,
QDRANT_API_KEY,
QDRANT_COLLECTION,
)
class QdrantVectorStore(VectorStore):
""" """
A wrapper around langchain_qdrant.Qdrant that implements the Wrapper around langchain_qdrant.Qdrant to provide a simple interface
VectorStore interface expected by LangChain chains. for adding documents and retrieving a retriever.
""" """
def __init__(self, embeddings, collection_name: str = None):
def __init__( self.collection_name = collection_name or config.QDRANT_COLLECTION
self, self.qdrant = Qdrant(
embeddings: Optional[OpenAIEmbeddings] = None, url=f"http://{config.QDRANT_HOST}:{config.QDRANT_PORT}",
collection_name: str = QDRANT_COLLECTION, api_key=config.QDRANT_API_KEY,
):
self.embeddings = embeddings or OpenAIEmbeddings()
self.collection_name = collection_name
# Initialize Qdrant client
self.client = Qdrant(
host=QDRANT_HOST,
port=QDRANT_PORT,
api_key=QDRANT_API_KEY,
collection_name=self.collection_name, collection_name=self.collection_name,
)
def add_documents(self, documents: Iterable[Document]) -> None:
"""
Add a collection of documents to the Qdrant store.
"""
texts = [doc.page_content for doc in documents]
metadatas = [doc.metadata for doc in documents]
ids = [doc.id for doc in documents if doc.id is not None]
# Embed the documents
embeddings = self.embeddings.embed_documents(texts)
# Upsert into Qdrant
self.client.upsert(
embeddings=embeddings, embeddings=embeddings,
documents=texts,
metadatas=metadatas,
ids=ids,
) )
def similarity_search( def add_documents(self, documents: list[Document]):
self,
query: str,
k: int = 5,
filter: Optional[dict] = None,
) -> List[Document]:
""" """
Perform a similarity search against the Qdrant store. Add a list of langchain Document objects to the Qdrant collection.
""" """
query_embedding = self.embeddings.embed_query(query) self.qdrant.add_documents(documents)
results = self.client.search(
query_embedding=query_embedding,
limit=k,
filter=filter,
)
# Convert results to Document objects
return [
Document(
page_content=result["payload"]["text"],
metadata=result["payload"],
id=result["id"],
)
for result in results
]
# The following methods are required by the VectorStore interface def get_retriever(self):
def embed_query(self, query: str) -> List[float]: """
return self.embeddings.embed_query(query) Return a retriever that can be used with LangChain chains.
"""
def embed_documents(self, documents: List[str]) -> List[List[float]]: return self.qdrant.as_retriever()
return self.embeddings.embed_documents(documents)