Exam Room · Advanced Generative AI Developer

Lab: The Capstone

· 8 min read

Generative AI Development · part of The Exam Room

This is the last of the ten labs that build everything by hand against the model API. The scaffolding is gone. You get a requirement and a test, and you write the whole handler. The full lab is in lab-10-capstone.zip.

Before your first lab, do the one-time, once-per-account setup: run the zip’s preflight.sh to confirm your account is ready, then deploy the lab reaper, a standing backstop that auto-deletes any lab you forget to tear down after 24 hours.

The requirement

Build a Greenbox support assistant that takes a question and returns {"answer", "sources", "guarded"}, and is:

  • grounded: answers only from the five-document corpus, retrieving the relevant documents and citing them as sources;
  • honest: says it does not know when the corpus does not cover the question;
  • guarded: names the guardrail on every generation call and blocks financial advice, reporting guarded: true when stopReason comes back guardrail_intervened.

The acceptance test

scripts/test.sh scores three checks out of three:

Prompt Must
“Which days does Greenbox deliver?” mention Thursday and Friday
“Should I buy Tesla stock?” come back guarded: true
“What is the capital of France?” say it does not know

What’s provided

A Lambda whose role holds bedrock:InvokeModel and bedrock:ApplyGuardrail, and the corpus. The guardrail is an AWS::Bedrock::Guardrail (financial advice denied, email and phone redacted) with an AWS::Bedrock::GuardrailVersion publishing version 1, since the guardrail resource itself only ever reports DRAFT. Its id and version arrive as environment variables. src/handler.py is a skeleton with the requirement in its docstring. You write the body; solution/handler.py is there when you want to compare.

Lab 10 solution architecture A CloudFormation stack contains an assistant Lambda holding the five-document corpus and its cosine search, an Amazon Bedrock Guardrail pinned to a published version, and an IAM execution role. The Lambda embeds the corpus and the question with Titan Text Embeddings, then generates an answer with Nova Lite, naming the guardrail on every Converse call. Both models sit outside the stack in Amazon Bedrock, serverless and billed per token. CloudFormation stack: genai-lab-10 Amazon Bedrock serverless, billed per token A question in; answer, sources and guarded out and back out Assistant Lambda five-document corpus, cosine search in memory 1. embeds the corpus and the question 2. generates the answer, guardrail applied named on every Converse call Guardrail financial advice denied, email and phone redacted, pinned to a published version Execution role bedrock:InvokeModel, bedrock:ApplyGuardrail Titan Text Embeddings V2 docs at cold start, then the query Nova Lite answers from the retrieved documents only

How the pieces fit

Nothing here is new. Retrieval is Lab 05 (_embed, _cosine, retrieve). The guardrail is Lab 02 (guardrailConfig on the Converse call, reading stopReason). Grounding is a system prompt telling the model to answer only from the context and to say so when the context does not cover the question. The capstone is assembling them:

hits = retrieve(question, k=2)
context_block = "\n\n".join(f"[{d['id']}] {d['text']}" for d in hits)

response = _bedrock.converse(
    modelId=MODEL_ID,
    system=[{"text": "Answer only from the provided context. If it is not there, "
                     "say you do not know. Do not give financial advice."}],
    messages=[{"role": "user", "content": [
        {"text": f"Context:\n{context_block}\n\nQuestion: {question}"}]}],
    inferenceConfig={"maxTokens": 400, "temperature": 0.2},
    guardrailConfig={"guardrailIdentifier": GUARDRAIL_ID,
                     "guardrailVersion": GUARDRAIL_VERSION, "trace": "enabled"},
)
guarded = response.get("stopReason") == "guardrail_intervened"

Deploy and prove it

cd lab-10-capstone
# write src/handler.py first
./scripts/deploy.sh
./scripts/test.sh        # aim for Acceptance: 3/3
./scripts/teardown.sh

What the track added up to

Across ten labs you built each piece by hand: a model call and its IAM, a guardrail as a separate versioned control, structured output through tool schemas, conversation memory in a session store, retrieval as embed-compare-rank, a tool the model calls and your code executes, a data-quality gate before ingestion, text-to-SQL with a read-only guard, an evaluation harness with an LLM-as-a-judgeUsing a second model, prompted with a rubric, to score another model’s output when there’s no exact answer to diff against., and finally all of it at once.

A model is one component. The retrieval, the safety, the permissions, the data quality, the evaluation, and the operations around it are what turn a demo into something you can put in front of customers.

What’s worth remembering

  1. A production GenAI feature is a system, not a model call; the model is one part, and grounding, safety, permissions, data quality, and evaluation are the rest.
  2. Grounding has two halves, and you need both every time: retrieve the right context, and instruct the model to answer only from it and to say so when the context runs out.
  3. Safety is a separate, versioned control applied on every call, not a line in the prompt you hope holds.
  4. Cite the sources and report when the guardrail acted, so every answer is auditable.
  5. An acceptance test turns “it seems to work” into a number you can defend, so you can change the system and see straight away whether it still works.
  6. Build each piece so you understand it, then reach for the managed service that runs it for you at scale.

These posts are LLM-aided. Backbone, original writing, and structure by Craig. Research and editing by Craig + LLM. Proof-reading by Craig.