Everything so far, in one small program
This lesson doesn't introduce anything new. It's a checkpoint: a small Q&A CLI over the same Nimbus Robotics policy docs Lesson 2 introduced, built entirely from pieces this course's beginner tier already covered.
| Lesson | Concept used here |
|---|---|
| 2 | SimpleDirectoryReader loads Documents from data/ |
| 3 | Settings.llm / Settings.embed_model, configured once, globally |
| 4 | VectorStoreIndex.from_documents() splits into Nodes and embeds them |
| 5 | response.source_nodes used to show which files backed an answer |
| 6 | (implicit) the retrieval step inside the chat engine's pipeline |
| 7 | (implicit) response synthesis, using the chat engine's default mode |
| 8 | index.as_chat_engine() and .chat(), memory across turns |
If any row in that table doesn't make sense on its own, that's the signal to go back to that lesson before continuing past this one.
Why a hardcoded question list instead of input()
A real CLI would read questions from input() in a loop. This lesson uses a hardcoded EXAMPLE_QUESTIONS list instead, run through the same ask() function, because a hardcoded list produces exactly reproducible output to capture in a README, an input() loop's output depends on what a person typed, which can't be captured ahead of time.
The code is structured so the swap is trivial: build_engine() and ask() don't know or care where a question string came from. A run_interactive() function at the bottom of lesson.py shows the real input() version, it isn't called by main(), but reading it shows the whole swap is two lines: replace the for question in EXAMPLE_QUESTIONS loop with a while True: input(...) loop.
The code, piece by piece
def build_engine(): documents = SimpleDirectoryReader(str(DATA_DIR)).load_data() index = VectorStoreIndex.from_documents(documents) return index.as_chat_engine()The full ingest -> index -> chat pipeline in three lines, Lessons 2, 4, and 8 respectively.
def ask(chat_engine, question: str) -> None: response = chat_engine.chat(question) print(f"Nimbus Assistant: {response.response.strip()}") sources = sorted({Path(n.metadata["file_name"]).name for n in response.source_nodes}) print(f" (sources: {', '.join(sources)})\n")A ChatEngine's response still carries .source_nodes, the same attribute Lesson 5's QueryEngine response had. This function shows both halves at once, the conversational answer and which policy files actually backed it, tying Lesson 5's source-citation idea into Lesson 8's chat loop.
Checkpoint
This closes out the beginner tier. Recapping the whole arc:
- Document -> Node -> Index -> QueryEngine/ChatEngine (Lesson 1) is the shape every lesson since has been building toward.
SimpleDirectoryReaderloads Documents;SentenceSplitter(used internally byVectorStoreIndex.from_documents) splits them into Nodes (Lesson 2).Settingsis the global object holding your LLM and embedding model, set once, read everywhere (Lesson 3).VectorStoreIndex.from_documents()embeds and stores Nodes, costing one embedding call per Node (Lesson 4).as_query_engine()/.query()run retrieve -> synthesize in one call and return aResponsewith.responseand.source_nodes(Lesson 5).as_retriever()/.retrieve()isolate retrieval alone, no LLM call, useful for debugging what an index actually finds (Lesson 6).response_mode(refine,compact,tree_summarize) controls how retrieved Nodes get synthesized into one answer (Lesson 7).as_chat_engine()/.chat()add conversation memory on top of the same pipeline, so follow-up questions work (Lesson 8).
From here, the course's next tier builds on this same foundation: persisting indexes so you don't re-embed on every run, agents and tool-calling, and more advanced retrieval and indexing strategies.
If anything here still feels unclear, ask before moving on to the next tier of this course.