This commit is contained in:
@@ -1,95 +1,98 @@
|
|||||||
# Agent with RAG Memory (ChromaDB)
|
# Agent with RAG Memory
|
||||||
|
|
||||||
This project implements a Retrieval-Augmented Generation (RAG) agent that uses **ChromaDB** as its sole vector store. The agent can ingest documents, store their embeddings, retrieve relevant passages, and generate answers using OpenAI’s GPT models.
|
This project implements a retrieval‑augmented generation (RAG) agent that uses **Qdrant** as the vector store and **Ollama** for local LLM inference.
|
||||||
|
The agent follows the latest LangChain API and is fully configurable via environment variables.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Vector Store** – Uses ChromaDB for storing and querying embeddings.
|
- **Qdrant** vector store (via `langchain-qdrant`)
|
||||||
- **Embeddings** – Generated with OpenAI’s `text-embedding-ada-002`.
|
- **Ollama** local LLM integration (via `langchain-ollama`)
|
||||||
- **Chat** – Generates responses with OpenAI’s `gpt-3.5-turbo`.
|
- Retrieval‑augmented generation with conversation memory
|
||||||
- **Public API** – The `Agent` class exposes `init`, `ingest`, and `ask` methods, keeping the original interface unchanged.
|
- Simple CLI interface for quick testing
|
||||||
|
|
||||||
## Setup
|
## Installation
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
```bash
|
# Create a virtual environment (optional but recommended)
|
||||||
git clone https://git.brojs.ru/kuzakhmetovartur/agent-s-rag-pamyatyu.git
|
python -m venv .venv
|
||||||
cd agent-s-rag-pamyatyu
|
source .venv/bin/activate # On Windows: .venv\\Scripts\\activate
|
||||||
```
|
|
||||||
|
|
||||||
2. **Install dependencies**
|
# Install dependencies
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
```bash
|
## Configuration
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Configure environment variables**
|
Create a `.env` file in the project root (or set environment variables directly):
|
||||||
|
|
||||||
Create a `.env` file in the project root (or export the variables in your shell):
|
```dotenv
|
||||||
|
# Qdrant
|
||||||
|
QDRANT_HOST=localhost
|
||||||
|
QDRANT_PORT=6333
|
||||||
|
QDRANT_API_KEY= # leave empty if no key
|
||||||
|
|
||||||
```dotenv
|
# Ollama
|
||||||
# ChromaDB
|
OLLAMA_HOST=localhost
|
||||||
CHROMA_URL=localhost
|
OLLAMA_PORT=11434
|
||||||
CHROMA_PORT=8000
|
OLLAMA_MODEL=llama3
|
||||||
|
|
||||||
# OpenAI
|
# Optional: collection name
|
||||||
OPENAI_API_KEY=YOUR_OPENAI_API_KEY
|
QDRANT_COLLECTION=documents
|
||||||
```
|
```
|
||||||
|
|
||||||
- `CHROMA_URL` and `CHROMA_PORT` point to your ChromaDB instance.
|
> **Note**: The Qdrant instance must be running and accessible at the specified host/port.
|
||||||
- `OPENAI_API_KEY` is required for embeddings and chat completions.
|
> The Ollama server must be running locally and expose the chosen model.
|
||||||
|
|
||||||
4. **Run ChromaDB**
|
|
||||||
|
|
||||||
Ensure a ChromaDB server is running on the specified host/port. You can start a local instance with Docker:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run -d -p 8000:8000 chromadb/chroma
|
|
||||||
```
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```js
|
### Adding Documents
|
||||||
const { Agent } = require('./src');
|
|
||||||
|
|
||||||
(async () => {
|
```python
|
||||||
const agent = new Agent();
|
from src.agent import Agent
|
||||||
await agent.init();
|
from langchain_core.documents import Document
|
||||||
|
|
||||||
// Ingest documents
|
agent = Agent()
|
||||||
await agent.ingest('The quick brown fox jumps over the lazy dog.', { source: 'example.txt' });
|
|
||||||
|
|
||||||
// Ask a question
|
docs = [
|
||||||
const answer = await agent.ask('What did the fox do?');
|
Document(page_content="Python is a programming language.", metadata={"source": "python.txt"}),
|
||||||
console.log(answer);
|
Document(page_content="LangChain is a framework for LLM applications.", metadata={"source": "langchain.txt"}),
|
||||||
})();
|
]
|
||||||
|
|
||||||
|
agent.add_documents(docs)
|
||||||
```
|
```
|
||||||
|
|
||||||
## API
|
### Querying the Agent
|
||||||
|
|
||||||
| Method | Description |
|
|
||||||
|--------|-------------|
|
|
||||||
| `init()` | Initializes the vector store (creates collection if needed). |
|
|
||||||
| `ingest(text, metadata)` | Adds a document to the vector store. |
|
|
||||||
| `ask(question)` | Retrieves relevant passages and generates an answer. |
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
If you have a test suite, run:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm test
|
python -m src.agent "What is LangChain?"
|
||||||
```
|
```
|
||||||
|
|
||||||
All tests should pass after the ChromaDB integration.
|
or from Python:
|
||||||
|
|
||||||
## Notes
|
```python
|
||||||
|
response = agent.run("What is LangChain?")
|
||||||
|
print(response["answer"])
|
||||||
|
```
|
||||||
|
|
||||||
- The agent’s public API remains unchanged; only the underlying vector store implementation has been swapped to ChromaDB.
|
The response will include the answer and the source documents used.
|
||||||
- No new external services are introduced beyond ChromaDB and the existing OpenAI usage.
|
|
||||||
- Ensure that the ChromaDB server is reachable; otherwise, the agent will throw connection errors.
|
|
||||||
|
|
||||||
---
|
## Project Structure
|
||||||
|
|
||||||
Happy coding!
|
```
|
||||||
|
agent-s-rag-pamyatyu/
|
||||||
|
├── src/
|
||||||
|
│ ├── agent.py
|
||||||
|
│ ├── config.py
|
||||||
|
│ └── vector_store.py
|
||||||
|
├── requirements.txt
|
||||||
|
├── pyproject.toml
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT License
|
||||||
+7
-11
@@ -1,19 +1,15 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "agent-s-rag-pamyatyu"
|
name = "agent-s-rag-pamyatyu"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "A simple RAG agent with interactive CLI using LangChain tools."
|
description = "Agent with RAG memory using Qdrant and Ollama"
|
||||||
authors = [
|
|
||||||
{ name = "Artur Kuzakhmetov", email = "artur@example.com" }
|
|
||||||
]
|
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.8"
|
requires-python = ">=3.9"
|
||||||
dependencies = [
|
|
||||||
"langchain>=0.0.0",
|
|
||||||
"python-dotenv>=0.21.0"
|
|
||||||
]
|
|
||||||
|
|
||||||
[project.scripts]
|
[project.dependencies]
|
||||||
agent-s-rag = "src.main:main"
|
langchain = ">=0.1.0"
|
||||||
|
langchain-qdrant = ">=0.1.0"
|
||||||
|
langchain-ollama = ">=0.1.0"
|
||||||
|
python-dotenv = ">=1.0.0"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["setuptools>=42", "wheel"]
|
requires = ["setuptools>=42", "wheel"]
|
||||||
|
|||||||
+4
-3
@@ -1,3 +1,4 @@
|
|||||||
# No external dependencies are required for this project.
|
langchain>=0.1.0
|
||||||
# The implementation uses only the Python standard library.
|
langchain-qdrant>=0.1.0
|
||||||
# If you wish to add optional dependencies, list them here.
|
langchain-ollama>=0.1.0
|
||||||
|
python-dotenv>=1.0.0
|
||||||
+73
-72
@@ -1,93 +1,94 @@
|
|||||||
"""
|
"""
|
||||||
Core agent implementation.
|
Agent implementation using the latest LangChain API.
|
||||||
|
The agent uses a RetrievalQA chain backed by the Qdrant vector store
|
||||||
This module contains the main Agent class used throughout the project.
|
and the Ollama LLM for local inference.
|
||||||
The agent maintains a registry of tools that can be invoked during
|
|
||||||
execution. The KnowledgeBaseTool is registered here so that the agent
|
|
||||||
can interact with the knowledge base without modifying the core logic.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Callable, Dict, Any
|
from __future__ import annotations
|
||||||
|
|
||||||
# Import the KnowledgeBaseTool but do not alter existing logic
|
from typing import Any
|
||||||
from .knowledge_base import KnowledgeBaseTool
|
|
||||||
|
from langchain_ollama import Ollama
|
||||||
|
from langchain.chains import RetrievalQA
|
||||||
|
from langchain.memory import ConversationBufferMemory
|
||||||
|
from langchain.prompts import PromptTemplate
|
||||||
|
|
||||||
|
from .config import OLLAMA_HOST, OLLAMA_PORT, OLLAMA_MODEL
|
||||||
|
from .vector_store import QdrantVectorStore
|
||||||
|
from langchain_core.documents import Document
|
||||||
|
|
||||||
|
|
||||||
class Agent:
|
class Agent:
|
||||||
"""
|
"""
|
||||||
A simple agent that can execute registered tools.
|
A simple retrieval-based agent that answers user queries
|
||||||
|
using documents stored in Qdrant and an Ollama LLM.
|
||||||
The agent's tool registry maps tool names to callable objects.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self, collection_name: str = "documents"):
|
||||||
self.tools: Dict[str, Callable[..., Any]] = {}
|
# Initialize LLM
|
||||||
# Register core tools
|
self.llm = Ollama(
|
||||||
self._register_core_tools()
|
model=OLLAMA_MODEL,
|
||||||
|
base_url=f"http://{OLLAMA_HOST}:{OLLAMA_PORT}",
|
||||||
|
)
|
||||||
|
|
||||||
def _register_core_tools(self) -> None:
|
# Initialize vector store
|
||||||
|
self.vector_store = QdrantVectorStore(collection_name=collection_name)
|
||||||
|
|
||||||
|
# Memory to keep conversation context
|
||||||
|
self.memory = ConversationBufferMemory(memory_key="chat_history")
|
||||||
|
|
||||||
|
# Prompt template
|
||||||
|
self.prompt = PromptTemplate(
|
||||||
|
input_variables=["chat_history", "question"],
|
||||||
|
template=(
|
||||||
|
"You are a helpful assistant. Use the following context to answer the question.\n"
|
||||||
|
"Context:\n{chat_history}\n"
|
||||||
|
"Question: {question}\n"
|
||||||
|
"Answer:"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# RetrievalQA chain
|
||||||
|
self.chain = RetrievalQA.from_chain_type(
|
||||||
|
llm=self.llm,
|
||||||
|
chain_type="stuff",
|
||||||
|
retriever=self.vector_store.as_retriever(),
|
||||||
|
memory=self.memory,
|
||||||
|
return_source_documents=True,
|
||||||
|
chain_type_kwargs={"prompt": self.prompt},
|
||||||
|
)
|
||||||
|
|
||||||
|
def add_documents(self, documents: list[Document]) -> None:
|
||||||
"""
|
"""
|
||||||
Register the default set of tools with the agent.
|
Add documents to the underlying vector store.
|
||||||
"""
|
"""
|
||||||
# Register the KnowledgeBaseTool under the name 'knowledge_base'
|
self.vector_store.add_documents(documents)
|
||||||
self.tools["knowledge_base"] = KnowledgeBaseTool()
|
|
||||||
|
|
||||||
def register_tool(self, name: str, tool: Callable[..., Any]) -> None:
|
def run(self, question: str) -> Any:
|
||||||
"""
|
"""
|
||||||
Register a new tool with the agent.
|
Run the agent on a user question.
|
||||||
|
Returns the LLM's answer and the source documents.
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
name : str
|
|
||||||
The name under which the tool will be registered.
|
|
||||||
tool : Callable[..., Any]
|
|
||||||
The tool instance or callable.
|
|
||||||
"""
|
"""
|
||||||
self.tools[name] = tool
|
result = self.chain({"question": question})
|
||||||
|
return result
|
||||||
|
|
||||||
def run_tool(self, name: str, *args, **kwargs) -> Any:
|
def __call__(self, question: str) -> Any:
|
||||||
"""
|
return self.run(question)
|
||||||
Execute a registered tool.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
name : str
|
|
||||||
The name of the tool to run.
|
|
||||||
*args, **kwargs
|
|
||||||
Arguments forwarded to the tool.
|
|
||||||
|
|
||||||
Returns
|
if __name__ == "__main__":
|
||||||
-------
|
import json
|
||||||
Any
|
import sys
|
||||||
The result of the tool execution.
|
|
||||||
|
|
||||||
Raises
|
# Simple CLI usage
|
||||||
------
|
agent = Agent()
|
||||||
KeyError
|
if len(sys.argv) > 1:
|
||||||
If the tool name is not registered.
|
query = " ".join(sys.argv[1:])
|
||||||
"""
|
else:
|
||||||
if name not in self.tools:
|
query = input("Enter your question: ")
|
||||||
raise KeyError(f"Tool '{name}' not found.")
|
|
||||||
tool = self.tools[name]
|
|
||||||
return tool(*args, **kwargs)
|
|
||||||
|
|
||||||
# Example method that uses the knowledge base tool
|
response = agent.run(query)
|
||||||
def get_fact(self, key: str) -> Any:
|
print("\nAnswer:\n", response["answer"])
|
||||||
"""
|
print("\nSources:")
|
||||||
Retrieve a fact from the knowledge base.
|
for doc in response["source_documents"]:
|
||||||
|
print(f"- {doc.metadata.get('source', 'unknown')}")
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
key : str
|
|
||||||
The key to look up.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
Any
|
|
||||||
The stored value.
|
|
||||||
"""
|
|
||||||
kb_tool: KnowledgeBaseTool = self.tools["knowledge_base"]
|
|
||||||
return kb_tool.query_entry(key)
|
|
||||||
|
|
||||||
# Additional agent logic would go here (omitted for brevity)
|
|
||||||
# ...
|
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
"""
|
||||||
|
Configuration helper for the agent project.
|
||||||
|
Loads Qdrant and Ollama connection details from environment variables
|
||||||
|
or a .env file. Provides a single source of truth for connection
|
||||||
|
parameters used throughout the codebase.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
# Load environment variables from .env if present
|
||||||
|
load_dotenv(dotenv_path=Path(__file__).parent.parent / ".env")
|
||||||
|
|
||||||
|
# Qdrant configuration
|
||||||
|
QDRANT_HOST: str = os.getenv("QDRANT_HOST", "localhost")
|
||||||
|
QDRANT_PORT: int = int(os.getenv("QDRANT_PORT", "6333"))
|
||||||
|
QDRANT_API_KEY: str | None = os.getenv("QDRANT_API_KEY") # Optional
|
||||||
|
|
||||||
|
# Ollama configuration
|
||||||
|
OLLAMA_HOST: str = os.getenv("OLLAMA_HOST", "localhost")
|
||||||
|
OLLAMA_PORT: int = int(os.getenv("OLLAMA_PORT", "11434"))
|
||||||
|
OLLAMA_MODEL: str = os.getenv("OLLAMA_MODEL", "llama3") # Default model
|
||||||
|
|
||||||
|
# Vector store collection name
|
||||||
|
QDRANT_COLLECTION: str = os.getenv("QDRANT_COLLECTION", "documents")
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
"""
|
||||||
|
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.vectorstores import VectorStore
|
||||||
|
from langchain_core.documents import Document
|
||||||
|
|
||||||
|
from .config import (
|
||||||
|
QDRANT_HOST,
|
||||||
|
QDRANT_PORT,
|
||||||
|
QDRANT_API_KEY,
|
||||||
|
QDRANT_COLLECTION,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class QdrantVectorStore(VectorStore):
|
||||||
|
"""
|
||||||
|
A wrapper around langchain_qdrant.Qdrant that implements the
|
||||||
|
VectorStore interface expected by LangChain chains.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
embeddings: Optional[OpenAIEmbeddings] = None,
|
||||||
|
collection_name: str = QDRANT_COLLECTION,
|
||||||
|
):
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
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,
|
||||||
|
documents=texts,
|
||||||
|
metadatas=metadatas,
|
||||||
|
ids=ids,
|
||||||
|
)
|
||||||
|
|
||||||
|
def similarity_search(
|
||||||
|
self,
|
||||||
|
query: str,
|
||||||
|
k: int = 5,
|
||||||
|
filter: Optional[dict] = None,
|
||||||
|
) -> List[Document]:
|
||||||
|
"""
|
||||||
|
Perform a similarity search against the Qdrant store.
|
||||||
|
"""
|
||||||
|
query_embedding = self.embeddings.embed_query(query)
|
||||||
|
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 embed_query(self, query: str) -> List[float]:
|
||||||
|
return self.embeddings.embed_query(query)
|
||||||
|
|
||||||
|
def embed_documents(self, documents: List[str]) -> List[List[float]]:
|
||||||
|
return self.embeddings.embed_documents(documents)
|
||||||
Reference in New Issue
Block a user