Where we left off
Every lesson script so far has been one flat main(). This lesson draws the same boundary naive_rag Lesson 23 and hybrid_rag Lesson 23 drew: a clean separation between expensive one-time setup and cheap per-question work, expressed as this series' shared Strategy protocol (docs/RAG-SERIES-PLAN/README.md). This course also names its two internal per-question steps, grade() and correct(), since they're substantial enough on their own to deserve names, even though only ingest() and ask() are the two functions the Strategy protocol itself requires.
The Strategy protocol, and what lives in this course's State
class Strategy(Protocol): def ingest(self, docs: list[Path]) -> object: ... # returns opaque State def ask(self, query: str, state: object, k: int = 2) -> str: ...This course's implementation:
def ingest(notes_dir: Path) -> CorrectiveState: ...def ask(query: str, state: CorrectiveState, k: int = 2) -> str: ...What's inside CorrectiveState, explicitly, so a future course (like adaptive_rag Lesson 21) can wire this in without reading the rest of this file:
@dataclassclass CorrectiveState: collection: chromadb.Collection # the retrieval index (Lessons 2-9's vector store, graduated to chromadb) grader: Grader # the retrieval evaluator (Lessons 3, 12), packaged as a small objectTwo fields, both required: collection is what _retrieve() queries, grader is what grade() calls to score each retrieved chunk. Nothing else, ask()'s rewrite step calls the module-level Gemini client directly (the same call_model() helper every lesson has used), it doesn't need its own slot in State.
The code, piece by piece
def grade(query: str, chunks: list[dict], grader: Grader) -> list[dict]: return [{**c, "grade": grader.grade(query, c["text"])} for c in chunks]Lesson 4's grade-every-chunk logic, now a named function taking a Grader instance instead of calling a bare module function.
def correct(query: str, graded_chunks: list[dict], state: CorrectiveState, k: int) -> tuple[str, list[dict]]: relevant = [c for c in graded_chunks if c["grade"] == "relevant"] if relevant: return query, relevant rewritten = call_model(REWRITE_PROMPT.format(question=query)).strip() ...Lessons 5-7's filter-or-rewrite logic, combined into one function: filter first, and only fall through to a rewrite-and-re-retrieve if nothing survived filtering. Returns both the effective query (possibly rewritten) and the chunks generation should actually use, mirroring Lesson 8's effective_query tracking.
def ask(query: str, state: CorrectiveState, k: int = 2) -> str: retrieved = _retrieve(query, state.collection, k) graded = grade(query, retrieved, state.grader) effective_query, relevant = correct(query, graded, state, k) ...The Strategy protocol's ask(): retrieve, grade, correct, generate, four calls, each one a named step from a lesson you've already done.
Checkpoint
ingest(notes_dir) -> CorrectiveState: build-the-index, run once.ask(query, state, k=2) -> str: answer-a-question, run per question. This is the series' shared Strategy shape, matchingnaive_ragandhybrid_ragLesson 23's exact two-function boundary.State = (collection, grader), explicitly: a chromadb collection for retrieval, aGraderobject for correction. That's the whole contract a future course needs to know to wire this course's retriever in without readinggrade()orcorrect()'s internals.grade()andcorrect()are this course's own named internal steps, not part of the cross-course Strategy protocol itself, they exist because this course's per-question work is substantial enough to deserve names, the same wayhybrid_rag's RRF fusion logic lives inside its ownask()without becoming a third protocol function.
If anything here still feels unclear, ask before moving to Lesson 24.