A folder of documents
The rest of this course built one agent. This part builds the thing people actually ask for: something that answers questions about a set of documents, and admits it when the answer is not in them.
You have every LangGraph idea you need already. What is missing is the piece that finds the right page, and that is ordinary Python. Start there, with no graph at all.
The documents
Four help pages for a note taking app. Small enough to read, varied enough that finding the right one is a real question.
DOCS = {
"getting-started.md": "Install the app from the store, then sign in with your email address. Your notes sync automatically once you are signed in.",
"passwords.md": "To reset your password, open Settings and choose Reset password. A link is sent to your email, and it expires after one hour.",
"billing.md": "Plans are billed monthly. To cancel, open Settings, choose Billing, then Cancel plan. You keep access until the end of the month.",
"offline.md": "Notes written offline are saved on your device. They sync the next time you open the app with a connection.",
}In a real project these come off disk, and the loop that reads them is four lines. The dictionary is here so the lesson stays about finding, not about file handling.
import pathlib
DOCS = {p.name: p.read_text() for p in pathlib.Path("docs").glob("*.md")}Turning text into words worth matching
To compare a question with a page, reduce both to a set of words. Throwing away the most common English words stops every page matching on the and to.
COMMON = {"the", "a", "an", "to", "how", "do", "i", "my", "is", "it",
"of", "and", "in", "for", "you", "on", "can", "what", "about"}
def words(text):
return {w.strip(".,?").lower() for w in text.split()} - COMMONFinding the best page
Score every page by how many words it shares with the question, take the highest, and refuse to guess when nothing overlaps at all.
def search(question):
asked = words(question)
scored = [(len(asked & words(body)), name) for name, body in DOCS.items()]
best_score, best_name = max(scored)
if best_score == 0:
return None, ""
return best_name, DOCS[best_name]That last check is the important line. A search that always returns something will hand the model a page about billing when you asked about the weather, and the model will dutifully answer from it. Returning nothing is what makes lesson 34 possible.
Try it
questions = [
"How do I reset my password?",
"Can I cancel my plan?",
"What happens to notes written offline?",
"Who won the cricket?",
]
for question in questions:
name, _ = search(question)
print(f"{question:42} -> {name or 'nothing found'}")Three found, one refused, and the fourth is the one that matters. The question is perfectly reasonable and the answer is genuinely not in these documents.
Yes, this is a crude search
It matches words. It does not understand that cancel my subscription and end my plan mean the same thing, and a real system uses embeddings, which turn text into numbers so that similar meanings sit near each other.
Embeddings need a model, and a model needs a key, so the swap is shown rather than run. Everything around it stays exactly as it is.
from langchain_core.vectorstores import InMemoryVectorStore
from langchain.embeddings import init_embeddings
store = InMemoryVectorStore.from_texts(
texts=list(DOCS.values()),
embedding=init_embeddings("openai:text-embedding-3-small"),
metadatas=[{"source": name} for name in DOCS],
)
def search(question):
hit = store.similarity_search(question, k=1)[0]
return hit.metadata["source"], hit.page_content- Add a fifth document and ask a question that should find it.
- Take the
COMMONwords away and see which pages start matching everything. - Return the top two pages instead of one, and print both names.
You understood something today that you didn't yesterday.