ASDRP · Agentic RAG Track · Module 05
Swap the local embedder for cloud Ollama and build your first complete end-to-end RAG answer.
What we'll cover
tutorials/.envfind_dotenv() patternChatOllama — generator LLMOllamaEmbeddings — embedderRAG_PROMPT templaterag_answer() functionThe full picture
Modules 01–04 built retrieval. Module 05 completes the loop: cloud embedder + Augment → Generate.
Track 1 · The Migration
qwen3-embedding:0.6b produces better representations for financial/commodity text than a 384-dim general model.
Track 1 · Shared keys
tutorials/.env patterntutorials/.env once. Every module notebook calls
load_dotenv(find_dotenv()), which walks up the directory
tree and resolves to that single shared file.
tutorials/.gitignore ignores .env at the
parent level. No per-module gitignore line needed — and no risk of
accidentally committing real keys.
import os
from dotenv import load_dotenv, find_dotenv
# Walks UP → finds tutorials/.env automatically
load_dotenv(find_dotenv())
HAVE_KEYS = bool(os.environ.get("OLLAMA_API_KEY"))
if not HAVE_KEYS:
print("No key found — using frozen/ results.")
tutorials/.env format:
OLLAMA_API_KEY=<your cloud Ollama key>
# COHERE_API_KEY= ← added in Module 10
# METALS_API_KEY= ← added in Module 11
Track 2 · Building the stack
from langchain_ollama import ChatOllama, OllamaEmbeddings
os.environ.setdefault("OLLAMA_API_BASE", "http://localhost:11434")
chat_llm = ChatOllama(
model="nemotron-3-super:cloud",
base_url=os.environ["OLLAMA_API_BASE"],
temperature=0.0, # deterministic answers for reproducibility
)
lc_embeddings = OllamaEmbeddings(
model="qwen3-embedding:0.6b",
base_url=os.environ["OLLAMA_API_BASE"],
)
| Object | Model | Role |
|---|---|---|
chat_llm | nemotron-3-super:cloud | Generates the grounded answer |
lc_embeddings | qwen3-embedding:0.6b | Encodes corpus chunks and queries |
:cloud suffix routes through the cloud relay. Substitute locally-served names if running a local daemon.
Track 2 · Indexing
:memory:from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_qdrant import QdrantVectorStore
from langchain_core.documents import Document
from pathlib import Path
raw_docs = [{"source": p.name, "page_content": p.read_text()}
for p in sorted(Path("corpus").glob("*.md"))] # 8 files
splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=60)
lc_docs = [Document(page_content=piece, metadata={"source": d["source"]})
for d in raw_docs
for piece in splitter.split_text(d["page_content"])]
vector_store = QdrantVectorStore.from_documents(
lc_docs, embedding=lc_embeddings,
location=":memory:", collection_name="metals_kb")
from_documents sends one embedding call per chunk (~50–80 calls total). Small, but it does consume quota. Skip if no key — use frozen output.
Track 3 · First RAG answer
RAG_PROMPT template — why it mattersfrom langchain_core.prompts import ChatPromptTemplate
RAG_PROMPT = ChatPromptTemplate.from_template(
"You are a precise metals-markets tutor. "
"Answer using ONLY the context passages. "
"If the context does not contain the answer, "
"say you do not know.\n\n"
"Context:\n{context}\n\n"
"Question: {question}\n"
"Answer:"
)
Track 3 · First RAG answer
rag_answer() — retrieve → select → generatedef rag_answer(question: str, k: int = 10, top_n: int = 3) -> dict:
retriever = vector_store.as_retriever(search_kwargs={"k": k})
candidates = [d.page_content for d in retriever.invoke(question)]
# Module 10 replaces this slice with a Cohere rerank call:
contexts = candidates[:top_n]
block = "\n\n".join(f"[{i}] {c}" for i, c in enumerate(contexts, 1))
response = chat_llm.invoke(
RAG_PROMPT.format_messages(context=block, question=question)
).content.strip()
return {"response": response, "retrieved_contexts": contexts}
Track 3 · Safety net
import json
if HAVE_KEYS:
result = rag_answer("What factors drive gold prices higher?")
else:
with open("frozen/rag_answer.json") as fh:
result = json.load(fh)
print("(using cached illustrative result — "
"set OLLAMA_API_KEY in tutorials/.env to run live)")
print(result["response"])
{"response": "...", "retrieved_contexts": [...]}. It lets you read sensible output and keep going.
Always know when the number lies
rag_answer() call sends one embedding request + one chat-completion request. Indexing sends ~50–80 embedding calls. Use the frozen fallback if you are on a free-tier key and want to preserve quota.
Recap
tutorials/.env via find_dotenv()ChatOllama + OllamaEmbeddings:memory:RAG_PROMPT constrains the LLM to contextrag_answer(): retrieve → top-3 → generateRun the notebook: 05_first_real_rag.ipynb · Next: Module 06 — Why Evaluate? + RAGAS Setup