RAG Agent with Qdrant and Reranking in 10xGraph (Python)
In shortBuild a retrieval-augmented generation agent in Python with 10xGraph RAGAgent, a local Qdrant store, Gemini embeddings and optional Cohere or cross-encoder reranking.
- 5 min read
- 9 sections
- Updated
- v1.0.0rc4
- Markdown
This example builds a retrieval-augmented generation agent. It indexes ten support-policy chunks into a local Qdrant store, then answers questions with RAGAgent. Google gemini-2.5-flash writes the answers and gemini-embedding-001 embeds the documents. You can add Cohere or a local cross-encoder to rerank results.
What you will build
A support agent that answers questions from a small policy knowledge base. The script ingests ten text chunks into a local Qdrant store, then runs RAGAgent three ways: plain vector search, with Cohere reranking, and with a local cross-encoder reranker. Gemini writes the answers and also embeds the documents.
Key concepts
RAGAgent(store, agent, reranker, top_k, top_n): a prebuilt graph,RETRIEVEthen optionalRERANKthenSYNTHESIZE.create_local_qdrant_store: file-based Qdrant for the knowledge base.GoogleEmbedding: embeds chunks and queries withgemini-embedding-001.system_user_id("knowledge_base"): the reserved owner of shared documents.RAGAgentsearches as this owner unless you setstore_configorstore_config_builder.CohereRerankerandCrossEncoderReranker: rerankers that reordertop_kcandidates and keeptop_n.store_config_builder: builds the search config from the run config, for per-user or per-tenant knowledge bases.
Run it
git clone https://github.com/10xGraph/10xGraph.git
cd 10xGraph/agentflowpip install "10xgraph[google-genai,qdrant]"
export GEMINI_API_KEY=...
# optional rerankers
pip install cohere && export COHERE_API_KEY=...
pip install sentence-transformersNo Docker services are required.
python examples/rag/rag_example.pyThe Cohere and cross-encoder runs are skipped when their requirements are missing.
How the code works
The knowledge base
QDRANT_PATH = tempfile.mkdtemp(prefix="10xgraph-rag-")
store = create_local_qdrant_store(
path=QDRANT_PATH,
embedding=GoogleEmbedding(model="gemini-embedding-001"),
collection="support_kb",
)The store is a file-based Qdrant database. GoogleEmbedding turns each chunk into a vector. If you switch to another embedding model, re-index, because vector sizes differ.
async def ingest() -> None:
"""Index the chunks once. The shared knowledge base is owned by a reserved id."""
await store.asetup()
owner = {"user_id": system_user_id("knowledge_base")}
for chunk in DOCUMENTS:
await store.astore(owner, chunk, category="support-policy")Ingest once. Every chunk is written under the shared knowledge-base owner. The store requires an owner for every record, and RAGAgent searches as system_user_id("knowledge_base") by default, so these documents are visible to every user and never mix with per-user memory.
The agent
def build_agent() -> Agent:
# RAGAgent calls agent.execute(...) itself and injects the retrieved documents,
# so this is a plain Agent with no tools.
return Agent(model="gemini-2.5-flash", system_prompt=SYSTEM_PROMPT)RAGAgent calls the agent itself and injects the retrieved documents into the request, so this is a plain Agent with no tools. The system prompt tells it to answer only from the retrieved documents. The documents are fenced in a block marked as not-instructions and are never written to state.context, so the saved thread keeps the user’s original message.
The three runs
async def ask(title: str, rag: RAGAgent, question: str) -> None:
app = rag.compile()
result = await app.ainvoke({"messages": [Message.text_message(question)]}, {"thread_id": title})
print(f"--- {title}\nQ: {question}\nA: {result['messages'][-1].text()}\n")
await app.aclose() RAGAgent(store=store, agent=build_agent(), top_k=4),With no reranker, all top_k hits go to the model.
reranker = CohereReranker(api_key=os.environ["COHERE_API_KEY"])
await ask(
"cohere",
RAGAgent(store=store, agent=build_agent(), reranker=reranker, top_k=8, top_n=3),
"Can I return a damaged item?",
)With a reranker, retrieve more candidates than the model needs. top_k=8 pulls eight chunks from the store, and top_n=3 keeps the three the reranker scores highest. The script runs this only when COHERE_API_KEY is set.
reranker = CrossEncoderReranker("cross-encoder/ms-marco-MiniLM-L-6-v2")
await ask(
"cross-encoder",
RAGAgent(store=store, agent=build_agent(), reranker=reranker, top_k=8, top_n=3),
"Are sale items refundable?",
)The cross-encoder is a local model, so it needs no API call. It runs only when sentence-transformers is installed. To scope search per user instead of one shared base, pass store_config_builder=lambda cfg: \{"user_id": cfg["user_id"]\} and index with the same ids.
Full code
"""RAGAgent over a local Qdrant knowledge base, with optional rerankers.
Ingests a few support-policy chunks, then answers questions in three setups:
plain vector search, Cohere rerank (needs COHERE_API_KEY) and a local cross-encoder
(needs sentence-transformers). Gemini writes the answers and embeds the documents.
Run: python examples/rag/rag_example.py
"""
import asyncio
import os
import shutil
import tempfile
from dotenv import load_dotenv
from tenxgraph.core.graph import Agent
from tenxgraph.core.state import Message
from tenxgraph.prebuilt.agent.rag import CohereReranker, CrossEncoderReranker, RAGAgent
from tenxgraph.storage.store import GoogleEmbedding, create_local_qdrant_store, system_user_id
load_dotenv()
DOCUMENTS = [
"Returns are accepted within 30 days of purchase with a receipt.",
"Items must be in original packaging to qualify for a full refund.",
"Digital downloads are non-refundable once accessed.",
"Shipping costs are refunded only when the return is due to our error.",
"To start a return, email [email protected] with your order number.",
"Exchanges can be done in store or by mail within 60 days.",
"Sale items are final sale and cannot be returned or exchanged.",
"Gift cards and store credit are non-refundable.",
"Damaged or defective items are replaced free of charge at any time.",
"International orders may carry extra return shipping fees.",
]
SYSTEM_PROMPT = [
{
"role": "system",
"content": (
"You are a customer-support agent. Answer only from the retrieved "
"documents. If they do not contain the answer, say so."
),
}
]
QDRANT_PATH = tempfile.mkdtemp(prefix="10xgraph-rag-")
store = create_local_qdrant_store(
path=QDRANT_PATH,
embedding=GoogleEmbedding(model="gemini-embedding-001"),
collection="support_kb",
)
async def ingest() -> None:
"""Index the chunks once. The shared knowledge base is owned by a reserved id."""
await store.asetup()
owner = {"user_id": system_user_id("knowledge_base")}
for chunk in DOCUMENTS:
await store.astore(owner, chunk, category="support-policy")
def build_agent() -> Agent:
# RAGAgent calls agent.execute(...) itself and injects the retrieved documents,
# so this is a plain Agent with no tools.
return Agent(model="gemini-2.5-flash", system_prompt=SYSTEM_PROMPT)
async def ask(title: str, rag: RAGAgent, question: str) -> None:
app = rag.compile()
result = await app.ainvoke({"messages": [Message.text_message(question)]}, {"thread_id": title})
print(f"--- {title}\nQ: {question}\nA: {result['messages'][-1].text()}\n")
await app.aclose()
async def main() -> None:
await ingest()
# 1. Plain RAG. With no reranker, all top_k hits go to the model.
await ask(
"plain",
RAGAgent(store=store, agent=build_agent(), top_k=4),
"Can I return a digital download?",
)
# 2. Retrieve many candidates, let Cohere pick the best few.
if os.getenv("COHERE_API_KEY"):
reranker = CohereReranker(api_key=os.environ["COHERE_API_KEY"])
await ask(
"cohere",
RAGAgent(store=store, agent=build_agent(), reranker=reranker, top_k=8, top_n=3),
"Can I return a damaged item?",
)
else:
print("Skipping Cohere: set COHERE_API_KEY.\n")
# 3. Same idea with a local model, no API call.
try:
import sentence_transformers # noqa: F401
except ImportError:
print("Skipping cross-encoder: pip install sentence-transformers.\n")
else:
reranker = CrossEncoderReranker("cross-encoder/ms-marco-MiniLM-L-6-v2")
await ask(
"cross-encoder",
RAGAgent(store=store, agent=build_agent(), reranker=reranker, top_k=8, top_n=3),
"Are sale items refundable?",
)
shutil.rmtree(QDRANT_PATH, ignore_errors=True)
if __name__ == "__main__":
asyncio.run(main())Source on GitHub: examples/rag
Expected output
Illustrative output.
--- plain
Q: Can I return a digital download?
A: No. Digital downloads are non-refundable once accessed.
--- cohere
Q: Can I return a damaged item?
A: Yes. Damaged or defective items are replaced free of charge at any time.Using a different model
Change the model string in build_agent(), for example "claude-sonnet-5-5" with pip install "10xgraph[anthropic]", or "gpt-5-mini" with the openai extra. Embeddings are separate: swap GoogleEmbedding for OpenAIEmbedding if you prefer, and re-ingest, since vector sizes differ.
Next steps
Frequently asked questions
- Why does the example index with system_user_id("knowledge_base")?
- The store requires an owner for every record. RAGAgent searches as system_user_id("knowledge_base") by default, so shared documents indexed under that id are visible to every user and never mix with per-user memory.
- What do top_k and top_n do?
- top_k is how many candidates the store returns. With a reranker, top_n is how many of those are passed to the model. Without a reranker all top_k documents are used and top_n is ignored.
- Are retrieved documents saved in the thread?
- No. They are held in a per-run cache and added to the outgoing request only, so the saved thread keeps the user's original message.