Integrate retrieval
- Lesson 10
- 120 min
- Module 3: Build one complete user journey
Lesson 10 of 22 · Portfolio Capstone: Build, Evaluate and Ship an AI ProductIntegrate retrieval
1. Select and bound the project
2. Prepare data and architecture
3. Build one complete user journey
4. Add reliability and access control
5. Evaluate and improve
6. Deploy and operate
7. Prepare the portfolio presentation
What you will be able to do
Replace the two stand-ins with the real parts: search by meaning in PostgreSQL, real answers of a model, citations checked in code, and a trace of every question that shows where its time goes.
Before you startWhat to know and to have ready
What you need to know
- Embeddings and dense search, from Embeddings for search, and vectors in PostgreSQL, from Vector storage.
- The context that goes to the model, from Assemble context.
- Answers made of claims, each with its passages, and what code can check about them, from Answer and cite.
- The slice and its page, from Backend foundation and Frontend foundation.
What you need on your computer
- Your project from the last lesson, with the pack loaded by the stand-ins. Stop the API for now (Ctrl+C in its terminal).
- An internet connection for the first run: the embedding model, about 470 MB, downloads once into
~/.cache/huggingface. - No account and no key. The answers in this lesson are recorded answers of a real model.
The real actor arrives
Section titled “The real actor arrives”At the end of the rehearsals, the real actor replaces the stand-in. The marks on the floor stay the same, and so do the cameras. But some things change: the actor is a little taller, so the lights move up. The crew changes one thing at a time and checks the scene after each change.
The capstone does the same. The interfaces stay: embed_queries and complete. The data behind them changes, and you check the slice after each of the two changes.
Change 1: the real embedding model
Section titled “Change 1: the real embedding model”In .env, replace the hash stand-in with the RAG course’s model, intfloat/multilingual-e5-small. Keep the mock for the answers, so only one thing changes.
EMBEDDER=e5ANSWER_PROVIDER=mockAsk the Ontario question in the terminal. scripts.ask calls the same pipeline as the API, in one process:
python -m scripts.ask "How long does delivery to Ontario take?" --tenant brambleInes (staff) at Bramble Books asks: How long does delivery to Ontario take?Passages found: noneAnswer (not answerable): The documents do not answer this question.none none | 28605 ms | tokens 0 in, 0 out | US$0.000000 | trace f6a04f8c45466eda7b5b6524b0b32b90The model’s loading lines are left out here and below. No passage, so the answer model is not called, and the answer is “The documents do not answer this question.” Nothing is broken. The documents were indexed with hash-384, and vectors of two models cannot be compared. So the search reads only the chunks of the current model, and there are none. Index again with the real model:
python -m scripts.load_pack --resetDeleted every shop's data.Identity: 2 shops, 7 people, 6 membershipslarkfield: 36 documents uploaded, 0 already therebramble: 15 documents uploaded, 0 already thereIndexing with intfloat/multilingual-e5-small@614241f ...
Indexed 51 documents in 6.2 s (failed 0). bramble: 15 documents, 60 passages, 0 failed larkfield: 36 documents, 185 passages, 0 failedThe first run downloads the model; later runs use the copy in the cache. The time changes from run to run: 7.1 s in the course’s next run, and about 30 seconds in a fresh environment in the course’s snapshot check. Now the same question finds passages. The search in doc_assistant/retrieve.py is the RAG course’s dense search, written as one SQL query. It applies four filters before it takes the best 5: the shop, the reader’s access level, the date of the question, and the embedding model. A passage that is filtered out can never take one of the five places. Ask the Ontario question again, with the mock still on:
Ines (staff) at Bramble Books asks: How long does delivery to Ontario take?Passages found: 20804b124392 delivery-policy v1, ffefe0c5ab86 delivery-policy v1, e5e28bf8b6a1 faq v4, 94a83a5b1bff delivery-policy v1, ba8746ede8c3 privacy-notice v2Answer (answerable): We ship within 2 business days. - We ship within 2 business days. [Delivery times, Shipping, v1]mock mock | 3822 ms | tokens 0 in, 0 out | US$0.000000 | trace b9b1f3c01c9332cf9e89a051938c3a27The mock still answers with the first sentence of the best passage. But now the best passage is the delivery policy’s “Shipping” section, found by meaning, not by shared words. Notice the time, 3,822 ms. Each scripts.ask is a new process, so each one loads the embedding model again. Keep that number in mind for the traces below.
Change 2: real answers, recorded
Section titled “Change 2: real answers, recorded”Now replace the mock with real answers:
ANSWER_PROVIDER=recordedThe course team asked a real model, chat-small, the course’s questions, and recorded each request and its answer in recordings/answers.jsonl. The recorded provider replays them: no key, no cost, and the same answer every time. A recording is found by the model, the prompt, the question and its date, not by the passages. The idea is the RAG course’s replay.
python -m scripts.ask "How long does delivery to Ontario take?" --tenant brambleInes (staff) at Bramble Books asks: How long does delivery to Ontario take?Passages found: 20804b124392 delivery-policy v1, ffefe0c5ab86 delivery-policy v1, e5e28bf8b6a1 faq v4, 94a83a5b1bff delivery-policy v1, ba8746ede8c3 privacy-notice v2Answer (answerable): Delivery to Ontario takes 4 to 8 days. Orders ship within 2 business days. - Delivery to Ontario takes 4 to 8 days. [Delivery times, Shipping, v1] - Orders ship within 2 business days. [Delivery times, Shipping, v1]recorded chat-small | 3932 ms | tokens 719 in, 137 out | US$0.000140 | trace 171024ffa6e415fc217bdea1792456dbA real model’s answer, with two claims, each cited. The last line shows the replay and what the real call cost: 719 tokens in and 137 out, US$0.000140 at chat-small’s price list (US$0.10 and US$0.50 per million tokens). The prompt is prompts/answer_v1.md. It asks for claims with passage IDs, in a strict JSON schema, so code can check them.
Recordings exist for the course’s 62 tuning questions with their dates, and for nothing else. Ask any other question, and the mock answers, with a note:
Ines (staff) at Bramble Books asks: Do we sell e-books?Passages found: 07e2059161e9 faq v4, 31d85d1940b3 faq v4, fafc42e3e194 special-orders v2, 5220ca4b5665 events-and-room-hire v1, 1e6a23c78c8f supplier-terms v1Answer (answerable): Can I pick up my online order? - Can I pick up my online order? [Frequently asked questions, Can I pick up my online order?, v4]Note: No recording for this question: the mock made this answer from your passages.mock mock | 3722 ms | tokens 0 in, 0 out | US$0.000000 | trace c40331669e8e4c26691b1e1cf0757815The date is part of the question. Larkfield’s questions were recorded with the date 2026-10-09, and the course’s “today” is 2026-10-11 (ASSISTANT_TODAY). The same words with another date are another question:
$ python -m scripts.ask "What is the staff discount?" --tenant larkfieldSam (staff) at Larkfield asks: What is the staff discount?Passages found: 81007010cd4d staff-discount-and-perks v1, 7919a06cc2aa staff-discount-and-perks v1, 93cb6359874a privacy-policy v3, 6fddb68adafa privacy-policy v3, 2638b732560d support-team-routing v4Answer (answerable): You get 20% off all products, except gift cards. - You get 20% off all products, except gift cards. [Staff discount and perks, Staff discount, v1]Note: No recording for this question: the mock made this answer from your passages.mock mock | 3791 ms | tokens 0 in, 0 out | US$0.000000 | trace 5c7e8e285f6de1af99ee1b84550d32a1$ python -m scripts.ask "What is the staff discount?" --tenant larkfield --as-of 2026-10-09Sam (staff) at Larkfield asks: What is the staff discount?Passages found: 81007010cd4d staff-discount-and-perks v1, 7919a06cc2aa staff-discount-and-perks v1, 93cb6359874a privacy-policy v3, 6fddb68adafa privacy-policy v3, 2638b732560d support-team-routing v4Answer (answerable): Staff get 20% off all products except gift cards, up to $2,000 of purchases per calendar year. It cannot be combined with other promotions or discount codes. - Staff get 20% off all products except gift cards, up to $2,000 of purchases per calendar year. [Staff discount and perks, Staff discount, v1] - The discount cannot be combined with other promotions or discount codes. [Staff discount and perks, Staff discount, v1]recorded chat-small | 3698 ms | tokens 958 in, 117 out | US$0.000154 | trace ddf0a76bf81ef75caf7388d2bd65deb1A safety rule for replay
Section titled “A safety rule for replay”A recording does not depend on the passages. So the provider could replay an answer made from staff-only passages to someone who may not read them, such as Omar, a read-only member from Module 4. The recorded provider has a rule against it: a recording is replayed only if every passage that it cites is among your passages. If one is not, the mock answers instead, and the note says why. In this module everyone acts as Sam, so you see the rule work in Module 4.
Your own model, if you want one
Section titled “Your own model, if you want one”ANSWER_PROVIDER=live calls any endpoint with the OpenAI Chat Completions format, with LLM_BASE_URL and LLM_API_KEY from .env. The settings never print the key. Each call has a timeout (PROVIDER_TIMEOUT_S, 20 seconds), and a failure comes back in the one error shape, for example 504 provider_timeout. This lesson did not run live: the recorded answers are real calls of the same model. Prices change: check your provider’s current price page first.
Citations, checked in code
Section titled “Citations, checked in code”You check a student’s footnotes. Does the cited page exist? Was the book on the reading list? Does the page say the number in the sentence? You cannot check whether the student understood the book, but you can check these things in seconds.
doc_assistant/cite.py does the same for every claim, as in Answer and cite. It checks five things. Each cited ID is a passage of this shop. The model got that passage. The claim cites something. Every number in the claim is in a cited passage. At least half of its words are there. Here is a tiny example. The passage is real, Bramble Books’ “Shipping” section. The four claims are made up to test the checks:
Passage 20804b124392:[20804b124392] Delivery times | policy, version 1, in force from 2026-09-15, no end date | section: ShippingDelivery timesShipping
We ship within 2 business days. Delivery in British Columbia takes 2 to 4 business days, and 4 to 8 days to the rest of Canada. We do not ship outside Canada.
Delivery to Ontario takes 4 to 8 days. cites ['20804b124392'] -> okDelivery to Ontario takes 3 to 5 days. cites ['20804b124392'] -> ['number_not_found:3,5']Delivery to Ontario takes 4 to 8 days. cites nothing -> ['no_citation']Delivery to Ontario takes 4 to 8 days. cites ['c0712b8decbc'] -> ['missing_id:c0712b8decbc']Read the four lines. The first claim is supported. The second has numbers that the passage does not have. The third cites nothing. The fourth cites a real passage, but from Larkfield’s delivery policy. In Bramble Books, that ID does not exist, so the check says missing_id, and the page shows nothing about it. These checks are clues, not proof: a claim can use the passage’s words and still say the opposite. A person decides those cases, and Module 5 measures them.
Where the time goes: traces
Section titled “Where the time goes: traces”A relay race has four runners. The team’s time is 41 seconds, but the coach wants to know more: which runner was slow, and where the baton changed hands. So the coach takes a split time for each runner.
The problem without split times: Sam’s first question through the API took 3.6 seconds. Tomás is sure: “The model is slow. Let’s pay for a faster one.” But the answer was a replay, which takes almost no time. Without a measurement, you would change the wrong part.
A trace is the record of one request, with a span for each step: a name, a start, a length and a few facts. The pipeline makes one trace per question with OpenTelemetry, the standard tool for traces. Its spans are ask, then retrieve, generate, check and store inside it. In the API, FastAPI adds its own spans around them: the request (POST /v1/tenants/{tenant}/answers), then fastapi.dependencies, fastapi.endpoint and fastapi.serialization. doc_assistant/tracing.py writes them to traces/spans.jsonl, one JSON object per line, so you need no other service to read them. The spans never contain the question or the answer, only IDs, sizes and times. A trace may be kept longer and read by more people than the data.
Sam started the API with the real parts and asked two questions at Bramble Books: the Ontario question, then “Is delivery free for a 75-dollar order?”. After each, Sam read the trace. The long attribute list of the first line is shortened here:
python -m scripts.trace lastThe first question:
trace 6c811aa755fae25bc492a985eed4c7ac: 3595 msPOST /v1/tenants/{tenant}/answers 0 ms + 3595 ms |########################################| http.route=/v1/tenants/{tenant}/answers, http.response.status_code=200 fastapi.dependencies 3 ms + 2 ms |# | code.function.name=doc_assistant.answers.post_answer fastapi.endpoint 5 ms + 3589 ms |####################################### | code.function.name=doc_assistant.answers.post_answer ask 5 ms + 3589 ms |####################################### | tenant=bramble, question.chars=39, prompt=answer_v1, answerable=True retrieve 5 ms + 3584 ms |####################################### | passages=5 generate 3590 ms + 0 ms | #| provider=recorded, model=chat-small, attempts=1, tokens.input=719, tokens.output=137 check 3590 ms + 2 ms | #| claims=2, claims.flagged=0 store 3592 ms + 2 ms | #| fastapi.serialization 3594 ms + 0 ms | #| code.function.name=doc_assistant.answers.post_answerThe second question, a few seconds later:
trace 7c3ad66b436e9bce03c290390269e646: 16 msPOST /v1/tenants/{tenant}/answers 0 ms + 16 ms |########################################| http.route=/v1/tenants/{tenant}/answers, http.response.status_code=200 fastapi.dependencies 0 ms + 2 ms |##### | code.function.name=doc_assistant.answers.post_answer fastapi.endpoint 3 ms + 13 ms | ############################### | code.function.name=doc_assistant.answers.post_answer ask 3 ms + 13 ms | ############################### | tenant=bramble, question.chars=39, prompt=answer_v1, answerable=True retrieve 3 ms + 8 ms | ################## | passages=5 generate 12 ms + 0 ms | # | provider=recorded, model=chat-small, attempts=1, tokens.input=723, tokens.output=115 check 12 ms + 2 ms | #### | claims=2, claims.flagged=0 store 13 ms + 2 ms | ##### | fastapi.serialization 16 ms + 0 ms | #| code.function.name=doc_assistant.answers.post_answerText description of the diagram
The first question took 3,595 ms in the API, and its retrieve step took 3,584 ms of them: the API loaded the embedding model at its first search. Generate took 0.1 ms, because the answer was replayed; check took 1.5 ms and store 1.7 ms. The second question took 16 ms in all. At the same scale its bars are thin lines; zoomed, retrieve takes 7.7 ms, generate 0.1 ms, check 1.6 ms and store 2.2 ms. The two recorded model calls took 1,463 ms and 1,420 ms when the course team made them. A live call would add about 1.4 seconds to generate.
So the slow part was retrieve: the API loads the embedding model at the first search, not at start-up. The answer carries both times. latency_ms is the real time of the question in the API: 3,587 and 11 ms. generation_ms is the time of the recorded call when the course team made it.
When to use traces. On every request that has more than one slow step, from the first day. They cost little: one line per span in a file. When not to. Do not put personal data in a span. What they cost. A file that grows with every question. Delete it, or send the spans to a collector with a retention limit; the planned course MLOps and LLMOps: Operating AI in Production covers collectors.
The whole slice, with real numbers
Section titled “The whole slice, with real numbers”Text description of the diagram
Upload path: the API checks the file and answers 202 in 12 ms, and the row waits as uploaded. The worker parses, chunks and embeds it with e5, and saves it as indexed (the whole pack: 51 documents in 7.1 seconds). Question path: the API checks the question. Retrieve keeps the top 5 after four filters, in 8 ms (3.6 seconds the first time). Generate replays chat-small’s answer (719 tokens in, 137 out, US$0.000140). The citation checks take 2 ms, and storing takes 2 ms. Every query to PostgreSQL names the shop.
Grace’s journey on the page now uses the real parts. The question has the date of the recordings:


Common mistakes
Section titled “Common mistakes”Common mistake
Every answer says 'No recording for this question'
Symptom: the answers on the page come from the mock, with the note “No recording for this question: the mock made this answer from your passages.”
Cause: the question, its date, the prompt or the model is not exactly as recorded. On the page, an empty date field means today, 2026-10-11, and Larkfield’s questions were recorded for 2026-10-09.
Fix: set the date to 2026-10-09 for Larkfield’s questions. Ask the questions exactly as in evaluation/dev.jsonl. For any other question, the mock is the honest answer without a model; use live if you have your own endpoint.
Common mistake
The first question takes seconds, and later ones do not
Symptom: the first answer after a start takes 3 to 4 seconds. The trace shows the time in retrieve.
Cause: the embedding model loads at the first search. This is expected, and the trace proves it.
Fix: none is needed for the slice. If the first user must not wait, load the model at start-up: one query to the embedder in the API’s lifespan. The cost is a slower start. Write the choice down.
Your project
Section titled “Your project”- Standard track. Make both changes, one at a time, and check after each. Ask three of the recorded questions on the page, and one of your own, which gets the mock with its note. Read the trace of each.
- Own-idea track. Replace your stand-ins one at a time. Index your documents with a real embedding model, and check that the search filters by tenant, access and date before the top k. For answers, record real answers of a small model for your tuning questions only, never for your held-out questions. Or keep the mock until Module 5, and say so.
Evidence to keep. In docs/evidence/m03-retrieval.txt: the load_pack output with the real model, one scripts.ask output with a cited answer, the trace of a first and a second question, and the output of the citation checks on one constructed bad claim. Label every mock or recorded answer as such.
Checklist
- The index was made with the same model that searches it.
- The search filters by shop, access level and date before it takes the top k.
- Every answer says which provider made it: mock, recorded or live.
- Every claim’s citations are checked in code, and a citation of another shop is
missing_id. - Every question has a trace, with no question text or answer in it.
Module 3 practice
Section titled “Module 3 practice”Guided practice: one small flow, end to end
Section titled “Guided practice: one small flow, end to end”Sam does one small flow from the upload to the trace, before adding anything. load_pack --reset deleted every shop’s data, the holiday hours too. So Sam uploads holiday-hours.md again, now with the real parts, and asks Grace’s question.
- Start the API again in its terminal (
fastapi dev doc_assistant/main.py), so that it reads the new.env. Uploadholiday-hours.mdto Larkfield, on the page or with thecurlcommand of Backend foundation. - Watch its status: it goes from
uploadedtoindexed, with its number of passages. - Ask “Is the help desk open on 26 December?”.
- Run
python -m scripts.trace last, and check that its trace ID is the one under the answer.
Show what Sam saw
The course’s run, with curl for the upload: its status line, then the document 3 seconds later, shortened.
HTTP/1.1 202 Accepted{"doc_key": "holiday-hours", "version": "1", "status": "indexed", "chunk_count": 2, "error": null}The answer, shortened to its main fields:
{ "answer": "The help desk is closed on 25 December, 26 December and 1 January.", "provider": "mock", "model": "mock", "input_tokens": 0, "output_tokens": 0, "cost_usd": 0.0, "latency_ms": 12, "generation_ms": 0, "trace_id": "c2fe0f20d60f387a39946f27c9f3ae9c"}trace c2fe0f20d60f387a39946f27c9f3ae9c: 18 ms generate 13 ms + 0 ms | provider=mock, model=mock, attempts=1, tokens.input=0, tokens.output=0The trace ID of the answer and of scripts.trace last are the same, so this trace belongs to this answer. Look at the provider: mock. Nobody recorded a real answer to this question, so the mock answered, and the page shows the note “No recording for this question”. This time the mock’s first sentence is right: the e5 search ranked “Closed days” first. That is luck, not quality. One small flow works end to end, and its weak point is written down: answers to new questions need the live provider.
Independent task
Section titled “Independent task”On your own project, complete one small flow end to end before you add any optional feature. The flow has four parts: one document of a new kind (for example a PDF with a table), and one question that only it answers. Then the answer with its citation on the page, and its trace. Then write in your README, under “Not yet”, three features that you will not add in this module, and why.
Show a worked solution
This is Sam’s worked solution on the standard track. The new kind of document is Bramble Books’ room-hire PDF, whose prices are only in a table. It is already in the pack, and its question was recorded.
$ curl -s -H "Content-Type: application/json" -d '{"question": "Can a community group use the reading room for free?"}' http://127.0.0.1:8000/v1/tenants/bramble/answers{ "answer": "Yes. Community groups can use the reading room for free on Tuesdays from 6 pm to 9 pm.", "provider": "recorded", "model": "chat-small", "input_tokens": 760, "output_tokens": 75, "cost_usd": 0.0001135, "latency_ms": 11, "generation_ms": 1227, "trace_id": "e8836b56a46ac7edea822d97338e3b07"}The answer above is shortened to its main fields, and the trace to the lines from ask down:
trace e8836b56a46ac7edea822d97338e3b07: 18 ms ask 5 ms + 13 ms | ############################ | tenant=bramble, question.chars=52, prompt=answer_v1, answerable=True retrieve 5 ms + 9 ms | ################## | passages=5 generate 14 ms + 0 ms | # | provider=recorded, model=chat-small, attempts=1, tokens.input=760, tokens.output=75 check 14 ms + 2 ms | ### | claims=1, claims.flagged=0 store 16 ms + 2 ms | ### |In the full answer, the one claim cites events-and-room-hire, section “Prices”: the table in the PDF. Sam’s README then gets a “Not yet” section:
## Not yet
- Sign-in and roles: every request acts as Sam (Module 4 adds them; until then, run it only on your computer).- A reranker: not measured yet; Module 5 decides with evidence.- Answers to new questions with a real model: they need the live provider and a key; the mock answers them now, labelled.On the own-idea track, your flow and your list are different; the shape is the same: one flow with its evidence, and a short list of what waits, each with its reason.
Completion check: a new user can do the task
Section titled “Completion check: a new user can do the task”Ask a person who has not seen your project to do the journey with the synthetic data. They use only your README: start the project, upload a document, see it indexed, ask a question and read the cited answer. Do not help, and write down each place where they stop. You pass the check when they finish without your help. If they stopped, fix the README at each place, and ask a second person.
Show what a good record looks like
This record is an example, made up to show the format.
Who: Amira (developer, had not seen the project). Computer: Windows 11, Python 3.12.Started: README "First run", step 1.Stop 1: step 2 - "pip install" took 6 minutes; she thought it had frozen. Fix: the README now says "about 900 MB; it can take several minutes".Stop 2: the page showed "No recording for this question" for her own question. Fix: the README now says which questions are recorded, and that others get the mock.Finished: upload (indexed, 4 passages), question, cited answer. 25 minutes.Second person after the fixes (Mei): finished with no stop.Knowledge check
Section titled “Knowledge check”Check your understanding
After EMBEDDER=e5, the Ontario question found no passage until Sam ran load_pack --reset. Explain why, and why the search does not simply use the old vectors.
Show the answer
The old chunks had vectors from the hash stand-in, and the question’s vector came from e5. Vectors of two models are in different spaces, even with the same length (384), so their distance means nothing. The search therefore reads only the chunks whose document was indexed by the current model (d.embedder = %(embedder)s), and there were none. Re-indexing made e5 vectors for every chunk. Searching the old vectors would return passages in a random order, which is worse than none.
Does your answer have the main points of ours?
Sign in and enroll in the course to save your answers. Every check counts toward the certificate.
Check your understanding
Predict: Sam asks 'What is the staff discount?' on the page with an empty date field. Which provider answers, and why?
Show the answer
The mock, with the note “No recording for this question”. An empty date means the course’s today, 2026-10-11, but the recording of this Larkfield question was made for 2026-10-09. The date is part of the replay key, because a policy can change between two dates. With the date 2026-10-09, the recorded answer of the real model is replayed.
Does your answer have the main points of ours?
Sign in and enroll in the course to save your answers. Every check counts toward the certificate.
Check your understanding
The first question took about 4 seconds. Tomás wants a faster model. Use the trace to answer him.
Show the answer
The trace splits the 4 seconds: almost all of it is in retrieve, which loads the embedding model at the first search. generate took almost no time, because the answer was a replay; the second question took a few milliseconds of retrieval. A faster answer model would change nothing here. If the first user must not wait, load the embedding model at start-up instead.
Does your answer have the main points of ours?
Sign in and enroll in the course to save your answers. Every check counts toward the certificate.
Check your understanding
Module completion check: a new user does the journey with the synthetic data. Which three pieces of evidence show that they did it, and which output of the slice must be labelled?
Show the answer
For example: the document’s row with status indexed and its number of passages; the answer on the page with its claims and citations; and the trace of that question, whose ID matches the one under the answer. Every answer must show its provider, so nobody reads a mock answer as the product’s quality. The record of where the new user stopped, and how the README was fixed, completes the evidence.
Does your answer have the main points of ours?
Sign in and enroll in the course to save your answers. Every check counts toward the certificate.
Recap
- Replace stand-ins one at a time, and check the slice after each change.
- An index must be searched with the same model that built it. Re-index after a change of model.
- The search filters by shop, access level, date and model before it takes the top 5.
- Recorded answers are real answers, replayed by model, prompt, question and date. Other questions get the mock, with a note. A recording is replayed only if your passages contain every passage that it cites.
cite.pychecks every claim in code: the ID exists in this shop, it was given to the model, the numbers and most words are in the passage.- A trace with a span per step shows where the time goes. The first question loads the embedding model; the replayed model call takes almost no time.
Module 3 in one paragraph. You built a vertical slice: a checked backend with one error shape, uploads as jobs with states, and a page that shows every state. Then the real parts replaced the stand-ins, one at a time: dense search, recorded answers of a real model, citations checked in code, and a trace per question. A new user can do the task with the synthetic data. But every request acts as Sam, in any shop.
Next, Module 4 makes the slice safe: sign-in, memberships, roles and row-level security.
Next: Permission enforcement