Lab 03 — Get structured JSON out of a model with tool use
Scaffold: 4/5. Same infrastructure as Lab 01. The gap is bigger this time: you wire up a tool schema and parse the result, not just add one argument.
The scenario
A support system needs to turn each incoming free-text message into a structured record it can route: an intent, the product mentioned, and an urgency. Asking the model for JSON in the prompt gets you close, but every so often it wraps the JSON in prose or a code fence and the parser downstream breaks. Tool use fixes that: you declare the exact shape you want, and the runtime returns typed arguments.
The requirement
POST a support message, get back a structured record:
{ "record": { "intent": "billing", "product": "Pro", "urgency": "high" } }
shaped by a schema instead of by pleading in the prompt.
What’s provided
template.yaml— the Lab 01 function and Bedrock-invoke role, unchanged.src/handler.py— the request parsing, response helpers, and theTICKET_TOOLschema (intent enum, product, urgency enum) are written. The gap is using the tool and reading the result.solution/handler.py— the reference answer.scripts/— deploy, test, teardown.
Your task
In src/handler.py:
- Pass
toolConfig={"tools": [TICKET_TOOL]}toconverse(), with a short system prompt telling the model to record the request via the tool. - Find the block in
response["output"]["message"]["content"]that has atoolUsekey; its["toolUse"]["input"]is your typed record. - Return it, and return an error if the model answered in prose instead of calling the tool, so you notice the failure rather than shipping it.
Run it
./scripts/deploy.sh
./scripts/test.sh
./scripts/test.sh "Please cancel my subscription, no rush."
./scripts/teardown.sh
What success looks like
test.sh returns a record object whose fields come straight from the schema
(intent from its enum, urgency low/medium/high) and then checks them: the
script exits non-zero if the function raised, if no record came back, or if a
value lands outside its enum. Try a message with no clear product and watch it
omit the optional field but still fill the required ones.
The enum steers the model hard, and at temperature: 0 a fourth urgency value
is unlikely. It is not impossible. Plain tool use hands back whatever the model
put in the toolUse block without validating it against the schema; Bedrock’s
strict tool use ("strict": true on the tool definition, where the model
supports it) is the opt-in that adds that validation. Without it, the assertion
in test.sh is what turns the shape into a guarantee.
If it fails
model did not call the tool— add the system prompt instructing tool use, and make sure you passedtoolConfig. Some models need the nudge.ValidationExceptionabout tools — the model id you chose may not support tool use; pick one that does (most current chat models do).- A key error reading
toolUse— the tool block is one entry in acontentlist that can also contain text; iterate and match on thetoolUsekey rather than assuming position zero. FAIL: ... is outside the enum— the model returned a value the schema does not list. Rare with this schema attemperature: 0, and exactly why the script checks the record instead of trusting it.
Reveal the solution
SRC=solution ./scripts/deploy.sh && ./scripts/test.sh
What you just learned
- Reliable structured output comes from a tool schema, not from asking for
JSON in prose. The answer arrives as parsed arguments in a
toolUseblock instead of prose you have to dig JSON out of. - An
enumin the schema steers the model far harder than telling it to avoid a value. Plain tool use still does not validate what comes back, so check it in your own code, or opt into strict tool use where you can. - The response
contentis a list of blocks; a tool call arrives as atoolUseblock you pull the typedinputout of. - This is the same mechanism an agent uses to call its action-group tools, so you have just built the core of function calling by hand.
Next
Lab 04 — Stream a response token by token. You move from a single blocking
call to ConverseStream, so the answer starts arriving while it is still being
generated.