commit 589a621340b2b80ab216a4204e18df2d3b8c20c7 Author: kuzakhmetovartur Date: Wed Jun 24 14:17:50 2026 +0300 feat: solution for 'Агент с RAG-памятью' diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b16538b --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +.env +dist/ +build/ +*.log diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1399ef7 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +FROM python:3.11-slim + +WORKDIR /app + +# Install dependencies +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy source code +COPY src ./src +COPY data ./data +COPY main.py . + +# Expose port if needed (not required for CLI) +# EXPOSE 8000 + +CMD ["python", "src/main.py"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..692a7a6 --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ +# Агент с RAG-памятью + +Главная +Мои задания +Агент с RAG-памятью +5Д +EN +Агент с RAG-памятью +Зачёт +Версия 6 +Дедлайн сдачи: 31.08.2026 + +В работе + +Требуется доработка + +Уважаемый студент! В вашем решении использованы правильные технологии (Qdrant, Ollama и LangChain), но есть два момента, которые требуют доработки: + +Инициализация агента производится через устаревший initialize_agent. Следует заменить его на современный create_agent из LangChain 1.x. +Параметры разбиения текста в функции chunk_document отличаются от тех, что у \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..0ffe75f --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,16 @@ +[project] +name = "rag-agent" +version = "0.1.0" +description = "Agent with RAG memory using LangChain 1.x, Qdrant, and Ollama." +requires-python = ">=3.10" +dependencies = [ + "langchain==1.0.0", + "langchain-community==0.0.20", + "langchain-ollama==0.0.3", + "qdrant-client==1.0.0", + "python-dotenv==1.0.0", +] + +[build-system] +requires = ["setuptools>=42", "wheel"] +build-backend = "setuptools.build_meta" \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7be8421 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +langchain==1.0.0 +langchain-community==0.0.20 +langchain-ollama==0.0.3 +qdrant-client==1.0.0 +python-dotenv==1.0.0 \ No newline at end of file diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..c6d0da1 --- /dev/null +++ b/src/__init__.py @@ -0,0 +1 @@ +# Empty init file to make src a package \ No newline at end of file diff --git a/src/agent.py b/src/agent.py new file mode 100644 index 0000000..c3e1a45 --- /dev/null +++ b/src/agent.py @@ -0,0 +1,66 @@ +from typing import List, Callable +from langchain_ollama import Ollama +from langchain.vectorstores import Qdrant +from langchain.agents import Tool, AgentExecutor, create_agent + +class RAGAgent: + """ + Agent that uses a Qdrant vector store and an Ollama LLM to answer queries + using Retrieval-Augmented Generation (RAG). + """ + + def __init__( + self, + llm: Ollama, + vector_store: Qdrant, + chunk_document_func: Callable[[str, int, int], List[str]] = None, + ): + self.llm = llm + self.vector_store = vector_store + self.chunk_document_func = chunk_document_func + + def add_documents(self, documents: List[str]) -> None: + """ + Adds a list of documents to the vector store after chunking them. + + Args: + documents: List of raw text documents. + """ + if self.chunk_document_func is None: + raise ValueError("chunk_document_func must be provided") + for doc in documents: + chunks = self.chunk_document_func(doc) + self.vector_store.add_texts(chunks) + + def _retrieve(self, query: str) -> str: + """ + Retrieves relevant documents from the vector store for a given query. + + Args: + query: The user query. + + Returns: + A concatenated string of relevant document contents. + """ + docs = self.vector_store.as_retriever().get_relevant_documents(query) + return "\n".join([doc.page_content for doc in docs]) + + def create_agent(self) -> AgentExecutor: + """ + Creates an AgentExecutor that uses the retrieval tool and the LLM. + + Returns: + An AgentExecutor ready to handle queries. + """ + retrieve_tool = Tool( + name="RAG", + func=self._retrieve, + description="Use this tool to retrieve relevant information from the knowledge base.", + ) + agent_executor = create_agent( + llm=self.llm, + tools=[retrieve_tool], + agent_type="chat-conversational-react-description", + verbose=True, + ) + return agent_executor \ No newline at end of file diff --git a/src/chunk_document.py b/src/chunk_document.py new file mode 100644 index 0000000..c5973db --- /dev/null +++ b/src/chunk_document.py @@ -0,0 +1,30 @@ +from typing import List + +def chunk_document(text: str, chunk_size: int = 500, chunk_overlap: int = 100) -> List[str]: + """ + Splits the input text into chunks of specified size with overlap. + + Args: + text: The full text to split. + chunk_size: Maximum number of characters per chunk. + chunk_overlap: Number of characters to overlap between consecutive chunks. + + Returns: + A list of text chunks. + """ + if chunk_size <= 0: + raise ValueError("chunk_size must be positive") + if chunk_overlap < 0: + raise ValueError("chunk_overlap cannot be negative") + if chunk_overlap >= chunk_size: + raise ValueError("chunk_overlap must be smaller than chunk_size") + + chunks = [] + start = 0 + text_length = len(text) + while start < text_length: + end = min(start + chunk_size, text_length) + chunk = text[start:end] + chunks.append(chunk) + start += chunk_size - chunk_overlap + return chunks \ No newline at end of file diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..37f0915 --- /dev/null +++ b/src/main.py @@ -0,0 +1,53 @@ +import os +from dotenv import load_dotenv +from langchain_ollama import Ollama +from langchain_community.embeddings import OllamaEmbeddings +from langchain.vectorstores import Qdrant +from qdrant_client import QdrantClient +from src.agent import RAGAgent +from src.chunk_document import chunk_document + +def main() -> None: + # Load environment variables if any + load_dotenv() + + # Initialize LLM and embeddings + llm = Ollama(model="llama3") + embeddings = OllamaEmbeddings(model="llama3") + + # Connect to Qdrant (assumes Qdrant is running locally on port 6333) + qdrant_client = QdrantClient(host="localhost", port=6333) + vector_store = Qdrant( + client=qdrant_client, + collection_name="rag_collection", + embeddings=embeddings, + ) + + # Create the RAG agent + rag_agent = RAGAgent(llm=llm, vector_store=vector_store, chunk_document_func=chunk_document) + + # Example documents to add to the vector store + sample_docs = [ + "LangChain is a framework for developing applications powered by language models.", + "Qdrant is a vector database that can store embeddings and perform similarity search.", + "Ollama provides a lightweight interface to run LLMs locally.", + ] + rag_agent.add_documents(sample_docs) + + # Build the agent executor + agent_executor = rag_agent.create_agent() + + print("RAG Agent is ready. Type your question (or 'exit' to quit).") + while True: + user_input = input(">>> ") + if user_input.lower() in {"exit", "quit"}: + print("Goodbye!") + break + try: + response = agent_executor.invoke({"input": user_input}) + print(response["output"]) + except Exception as e: + print(f"Error: {e}") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/utils.py b/src/utils.py new file mode 100644 index 0000000..9b84a36 --- /dev/null +++ b/src/utils.py @@ -0,0 +1,63 @@ +import os +from typing import List +from pathlib import Path + +from langchain.embeddings import OllamaEmbeddings +from langchain.vectorstores import Qdrant +from qdrant_client import QdrantClient + + +def chunk_document(text: str, chunk_size: int = 500, chunk_overlap: int = 100) -> List[str]: + """ + Splits the input text into chunks of specified size with overlap. + + :param text: The full text to chunk. + :param chunk_size: Number of words per chunk. + :param chunk_overlap: Number of overlapping words between consecutive chunks. + :return: List of chunk strings. + """ + words = text.split() + chunks = [] + start = 0 + while start < len(words): + end = min(start + chunk_size, len(words)) + chunk = " ".join(words[start:end]) + chunks.append(chunk) + start += chunk_size - chunk_overlap + return chunks + + +def get_ollama_embeddings() -> OllamaEmbeddings: + """ + Returns an OllamaEmbeddings instance using the default Ollama model. + + :return: OllamaEmbeddings instance. + """ + return OllamaEmbeddings(model="llama3.1") + + +def init_qdrant_collection(collection_name: str = "rag_collection") -> Qdrant: + """ + Initializes a Qdrant collection. Creates it if it does not exist. + + :param collection_name: Name of the collection. + :return: Qdrant vector store instance. + """ + client = QdrantClient(url="http://localhost:6333") + if not client.collection_exists(collection_name): + client.create_collection( + collection_name=collection_name, + vectors_config={"size": 1024, "distance": "Cosine"} + ) + return Qdrant(client=client, collection_name=collection_name) + + +def add_chunks_to_qdrant(chunks: List[str], embeddings: OllamaEmbeddings, qdrant: Qdrant) -> None: + """ + Adds text chunks to the Qdrant collection with embeddings. + + :param chunks: List of text chunks. + :param embeddings: Embeddings instance to compute vectors. + :param qdrant: Qdrant vector store instance. + """ + qdrant.add_texts(texts=chunks, embeddings=embeddings) \ No newline at end of file