Courses / AWS in Action: AI edition
Session 5 of 10
Tickets through tool use
Notes
Student Notes — Chapter 5: Tickets through tool use
AWS in Action: AI edition · Chapter 5
Before you start
Nothing is needed to read this chapter or to run its offline checks beyond Python and the development tools listed in the course repository’s requirements-dev.txt. The offline checks use a fake model and a fake table and read local files, so they need no AWS account and cost nothing. The live steps (deploying the stack and sending real messages to the model) need an AWS account, in a Region where Amazon Bedrock and S3 Vectors are both offered, because the stack still holds chapter 4’s knowledge base; they create real resources and cost money, so you delete the stack at the end. Do them from the AWS console first, with CloudShell, a terminal that opens in the browser, as the alternative; sending the messages is the one step with no console route, so it uses curl in CloudShell. Sign in as an IAM or IAM Identity Center user, not as the root user: a Bedrock step done as the root user can stop with an AccessDeniedException that names the root user. Before the live steps, check that you have a budget alert, a model that supports tool use (Amazon Nova Lite or Amazon Nova Micro) and Amazon Titan Text Embeddings V2 in the Bedrock Model catalog, and the course code on your computer, cloned or downloaded from the course repository. The steps are written out in two places: SETUP.md at the top of the course repository, and the section Before you try in chapters/05-tickets/README.md, which gives a console route beside each command. The tickets address has no sign-in until chapter 7, so use made-up names. This chapter builds on chapter 4, but it needs little of it. Recap: a Lambda function is code that runs when called, an API gives it an address, a template builds a stack that is created and deleted as one unit, and a bucket is a container for files. Have paper or a notes app open for the Flashcards and the Model answer.
Three ways to follow this chapter, the same as before, and you can mix them:
- Watch: read the chapter and follow the build on screen. Nothing needed.
- Copy and run: copy the folder
chapters/05-tickets/and run its checks. - Build with AI: open
chapters/05-tickets/prompts/, run each prompt in an AI coding tool, then run that prompt’s check. The checks for the whole chapter are inchapters/05-tickets/prompts/CHECK.md.
Summary
This chapter lets the helpdesk act. A request such as “Request Figma access” creates a ticket, and a question such as “What is the status of 1042?” reads one. The model does neither itself: it is shown two described actions, called tools, and answers with a request to run one. Your function runs the tool against a DynamoDB table, sends the result back, and the model writes the reply. A tool that fails or a request that lacks a detail comes back to the model as an error it can explain, not as a crash. You extend chapter 4’s template, try three messages, and delete the stack.
Key concepts
| Term | Definition |
|---|---|
| Tool | One action you describe to the model: a name, a plain description and the details it needs. |
| Tool use | The model answers with a request to run a tool, your code runs it, and the result goes back to the model, which then writes its reply. |
| Tool definition | The description of a tool that travels with each request: a name, a description and an input schema. |
| Input schema | The list of details a tool needs, each with a type, and which of them are required. |
| Tool request | What the model sends instead of an answer when it wants a tool run: the tool’s name, its input and an id. The model stops with the reason tool_use. |
| Tool result | What your function sends back for a tool request, with the same id: JSON when the tool worked, or text marked as an error when it did not. |
| DynamoDB table | A store of data in items. Each item is a group of attributes and is found by its primary key. |
| Item, attribute, primary key | An item is one record, such as one ticket. An attribute is one piece of it, such as the title. The primary key is the attribute that identifies the item; here it is TicketId. |
| Counter | One extra item in the table that counts tickets, so each new ticket gets the next number: 1001, 1002 and so on. |
| Error result | The text your function gives the model when a tool cannot do its job, marked as an error, so the model can read and explain it. |
tools_used |
The list in our reply of the tools that actually ran. It is how you check that a ticket number is real. |
Explanations
Orientation: where this chapter sits
Chapter 4 let the helpdesk read. This chapter lets it act, through code you wrote. In the finished helpdesk the model reaches ticket tools, and the tools change a real table:
Today you build the ticket tools part of the picture.
Try this first: an employee writes “Please request Figma access for me. My name is Priya.” The helpdesk should open a ticket. Who should write that ticket into the ticket system? The model, by writing “Done, ticket opened” in its reply; the model, by asking for a ticket to be opened while your function writes it and reports back; or the employee, after the model names the form to fill in. Pick one and say why. The chapter builds one of them.
By the end you can:
- explain how a message becomes a ticket and a ticket number, step by step;
- read the tool definitions and the handler and say what each part is for;
- send three messages, check the table, and delete the stack.
Part 1. The goal: tickets from plain requests
A manager says “file this”, and an assistant files it and reports back. That is the shape of this chapter. An employee writes “Request Figma access” and the helpdesk creates a ticket; the reply gives its number. The employee later asks “What is the status of 1042?” and the helpdesk reads that ticket and says where it stands. The tickets live in a DynamoDB table, with numbers like 1001 and 1002. The model does not touch the table. Your function does.
The code for this chapter is in the course repository under chapters/05-tickets/. The function is chapters/05-tickets/app/tickets/handler.py, its tests are in chapters/05-tickets/tests/test_tickets.py and chapters/05-tickets/tests/test_tickets_stack.py, and the stack description is chapters/05-tickets/template.yaml. Chapter 4’s files are still there. A prompts/ folder holds one prompt per build step with a check for each, and a prompt is finished when its check passes. This chapter adds an endpoint, POST /tickets, next to /ask. Use curl to talk to it; the chat page is unchanged.
The address is open. /tickets has no sign-in. Anyone who has its address can open tickets and look up any ticket number, and each message is billed to your account. Sign-in arrives in chapter 7. Until then, keep the throttle in the template, put no real names or real problems in a test message, and delete the stack as soon as you have finished.
Part 2. How tool use works
A diner tells the waiter what to order and the kitchen cooks it; the diner stays out of the kitchen. A manager writes “file this” and the assistant does the filing. In both, the person who decides is not the person who acts. Tool use works like that. You describe some actions to the model; the model answers with a request to run one; your code runs it; and the result goes back to the model. A tool is one such action: a name, a plain description and the details it needs. In Amazon Bedrock the model does not call the tool itself. It decides that one is needed, and your application code runs it and returns the result for the model to use in its final reply. With the Converse API this is called client-side tool calling: your code calls the tool on the model’s behalf.
The round trip, from the first message to the reply:
Read it from left to right: the employee’s message goes to the function together with the two tool descriptions; the model stops and asks to run create_ticket; the function runs it and saves the ticket in the table; the result goes back to the model; the model writes the reply. The reply is written last, after the result comes back.
Two messages sit in the middle. When the model wants a tool, it stops with the reason tool_use and sends a request. This one is illustrative:
{"toolUse": {"toolUseId": "abc1",
"name": "get_ticket_status",
"input": {"ticket_id": "1042"}}}
It holds the tool’s name, the input the model filled in, and an id. Your function sends back a result that carries the same id, so the model can match each result to its request. The result holds JSON when the tool worked, or text with the status error when it did not. The status field is documented as supported only by some model families, including Amazon Nova and Anthropic Claude 3 and 4, so check yours.
Here is a failure you should recognise before you build anything.
Situation: the reply that sounds done.
Employee: "Request Figma access. I'm Priya."
Model: "Done! Ticket 5000 is open."
tools_used: []
The reply says a ticket is open, but the model did not ask for the tool. This is chapter 1’s hallucination again: a confident wrong answer. The cause: the model can write the words without any action behind them. The function writes the table, so check tools_used before you trust a number. The list holds the tools that actually ran; here it is empty, so nothing was written.
Part 3. Define two tools
A menu card gives each dish a name and a short description. A paper form has labelled boxes, some marked required. A tool definition is both: it tells the model what an action is called, what it does and which details it needs. It has a name, a description and an input schema, the list of details, each with a type, and which of them are required. The definition is what the model knows about the tool, so write it clearly.
The first tool, create_ticket, opens a ticket. It needs two details, a title and the name of the person asking:
"name": "create_ticket",
"description": "Open a new IT ticket …",
# …
"required": ["title", "requester"],
What is marked required is what the model must supply. The second tool, get_ticket_status, looks up a ticket and needs one detail, ticket_id, whose description gives the example 1042. Both definitions sit in one structure, TOOL_CONFIG, in chapters/05-tickets/app/tickets/handler.py. A tool’s name may be up to 64 letters, digits, underscores and hyphens, and the top level of its input schema must be an object.
The definitions travel with each request. The function calls the same Converse API as chapter 2 and adds toolConfig:
response = bedrock.converse(
modelId=MODEL_ID, messages=messages,
system=SYSTEM, toolConfig=TOOL_CONFIG,
)
The file also sets a short length cap and a low temperature, as in chapter 2. No tool is forced: the model decides whether it needs one. The system text tells it to ask for a missing detail instead of guessing, and not to invent a ticket number or a status. The default model, Amazon Nova Lite, lists client-side tool calling as supported, and so does Nova Micro, the other value in the ModelId list; a model without tool use fails the request. The prompt chapters/05-tickets/prompts/02-tool-definitions.md writes the definitions.
Part 4. A table and a function that runs the tools
A filing cabinet holds one folder per ticket, labelled with its number. A ledger holds one line per ticket. A DynamoDB table works the same way: it stores data as items, an item is a group of attributes such as a title and a status, and each item is found by its primary key. Here the key is TicketId, one item per ticket. Give the number, get the ticket. The table is set to bill per request, so there is no capacity to size.
Tickets need numbers people can say aloud. One extra item, named counter, counts the tickets. The function adds one to it and reads the new value; if the item does not exist yet, DynamoDB starts from zero. The ticket number is 1000 plus the count, so the first ticket is 1001. These are the key lines of create_ticket:
UpdateExpression="ADD LastNumber :one",
ReturnValues="UPDATED_NEW",
# …
ConditionExpression="attribute_not_exists(TicketId)",
The last line makes the save conditional: it succeeds when no item has that number yet and fails when one does, so a number is not written twice. Adding to a counter is not safe to repeat: a retried call can add twice and skip a number. That is fine for a demo ticket and not fine for money. An item looks like this, in a sketch:
TicketId Title Requester Status
1001 Figma access Priya open
counter LastNumber = 1
The function ties the pieces together in a loop. It asks the model; if the stop reason is tool_use, it runs each tool the model requested and sends the results back in one message; then it asks again:
for _ in range(MAX_ROUNDS):
# … call converse, run the tools it asks for
if stop_reason != "tool_use":
return reply(200, {"answer": …})
MAX_ROUNDS is 3: ask, read the result, answer. A request that has no final answer after three model calls returns an error and stops, so a model that keeps asking for tools cannot run up the bill. The reply is {"answer": "...", "tools_used": ["create_ticket"]}. A message may be at most 1000 characters, and a ticket number must be 4 to 9 digits, which keeps the model from asking for the counter item. A status lookup returns the number, the title and the status, and leaves out who asked.
The prompt chapters/05-tickets/prompts/03-run-the-tools.md writes the tools and this loop.
Part 5. A tool that fails, a detail that is missing
A shop assistant who finds an item out of stock returns to say so, instead of vanishing. A form sent back carries a sticky note that says which box is empty. An error result is that sticky note: the answer your function gives the model when a tool cannot do its job, a short plain text, marked as an error, that the model can read and explain. A failed tool is an answer for the model, not a crash.
The code handles each kind of failure like this:
except ToolError as err:
result["content"] = [{"text": str(err)}]
result["status"] = "error"
except Exception:
logger.exception("tool %s failed", name)
A missing detail and an unknown ticket are raised as a ToolError, and their text goes back as the error result. Anything else is logged with its full cause, and the model gets a safe message about the ticket system being unavailable. The model hears what to say; the logs keep what happened.
Three situations show where this matters. You have already met the first.
Situation 1: the reply that sounds done. A reply names ticket 5000 and tools_used is empty. Cause: the model wrote the number itself and nothing ran.
Situation 2: the missing detail.
Employee: "I need a new monitor."
Tool: requester is missing -> error
No name was given, so a ticket would belong to nobody. Cause: the request lacks a required detail. The system text tells the model to ask who is asking. If it calls the tool anyway, the function returns a “Missing detail” error and creates nothing. Which path the model takes can differ from run to run, and both are safe.
Situation 3: the ticket that is not there.
Employee: "Status of ticket 7777?"
Tool: "There is no ticket 7777."
Nobody opened 7777. Cause: the lookup finds no item, and a model might otherwise invent a status. The function turns the missing item into an error result, so the model says there is no such ticket.
This table turns the three into a routine:
| Symptom | What it usually means | What you do |
|---|---|---|
A ticket number, but tools_used is empty |
The model wrote the number itself; nothing was saved | Do not trust it; look in the table; tighten the system text |
| “The assistant could not finish” | Three model calls passed with no final answer | Read the function logs; try a simpler message |
| The reply asks who is asking | A detail was missing and nothing was created | Answer it; this is the right behaviour |
Check tools_used before you trust a ticket number. The prompt chapters/05-tickets/prompts/04-tool-tests.md writes the tests for the round trip, a failing tool and a missing detail, with a fake model and a fake table.
What this chapter leaves open, and where the course deals with it:
| What is still open | What the helpdesk will do | Built in |
|---|---|---|
| Anyone with the address can open and read tickets | Sign-in and roles | Chapter 7 |
| The helpdesk does one thing at a time | A loop that works through a problem and escalates | Chapter 6 |
| Nobody watches cost or errors | Logs, cost and an alarm | Chapter 9 |
Part 6. Extend the template, test, delete
Templates in this course are cumulative: chapter 5 holds the resources chapter 4 held, plus what it adds. Chapter 4’s seventeen resources stay, and the CloudFront distribution gains a /tickets path. The file is chapters/05-tickets/template.yaml, and it adds seven:
| Piece | What it is for | Name in the template |
|---|---|---|
| Table | Holds the tickets and the counter | TicketsTable |
| Log group | Where the function writes its logs | TicketsFunctionLogGroup |
| Function role | What the function may do | TicketsFunctionRole |
| Function | Runs the model loop and the tools | TicketsFunction |
| API integration | Connects the route to the function | TicketsIntegration |
| Route | POST /tickets |
TicketsRoute |
| Invoke permission | Lets the API call the function | TicketsInvokePermission |
The function role is a staff badge with a short list of doors: it may write its own logs, call the chosen model, and get, put and update items in that one table. The table is tagged chapter: session-05, project and environment like the other resources that can carry a tag, and nothing has a Retain deletion policy. Resources are named from the Project and Environment parameters, such as helpdesk-dev-tickets, and the model permission follows the inference-profile route chosen in chapter 2. The template adds one output you use: TicketsTableName. The prompt chapters/05-tickets/prompts/01-tickets-table.md builds the table, and chapters/05-tickets/prompts/CHECK.md holds the checks for the whole chapter.
Costs money. The live steps create real resources, and each message calls the model, usually twice. Deploy, try and delete in one sitting.
aws cloudformation deploy \
--stack-name helpdesk-dev-ch5 \
--template-file template.yaml \
--capabilities CAPABILITY_IAM --parameter-overrides Project=helpdesk Environment=dev
Read the outputs into variables, as in chapter 4, and upload the chat page. Then send three messages:
curl -s -X POST "$SITE/tickets" -H "content-type: application/json" -d '{"message": "Please request Figma access for me. My name is Priya."}'
curl -s -X POST "$SITE/tickets" -H "content-type: application/json" -d '{"message": "What is the status of ticket 1001?"}'
curl -s -X POST "$SITE/tickets" -H "content-type: application/json" -d '{"message": "I need a new monitor."}'
The first reply gives a ticket number, and tools_used lists create_ticket. The second reports the ticket as open. The third leaves out who is asking, so the model should ask for a name instead of opening a ticket. To look in the table, scan it with aws dynamodb scan --table-name and the table name from the TicketsTableName output; you will see the ticket and the counter item. In the console, the same view is DynamoDB → Tables → your table → Explore table items. The page and the policy pages are not needed for tickets; upload them when you also want the chat page or /ask. These commands are also in chapters/05-tickets/README.md.
A hotel guest checks out and the room is cleared; a room left occupied keeps being charged. A stack you do not delete keeps costing money. The bucket still holds the chat page, and the documentation says a bucket can be deleted only if it has no objects in it, so empty it first:
aws s3 rm "s3://$BUCKET" --recursive
aws cloudformation delete-stack --stack-name helpdesk-dev-ch5
aws cloudformation wait stack-delete-complete --stack-name helpdesk-dev-ch5
The wait command polls until the stack status is DELETE_COMPLETE, so when it returns the stack is gone. Deleting the stack removes the table and its tickets, and the rest of what the stack made, because nothing has a Retain deletion policy.
Practice: what do you do?
Decide each one before reading the call: what you do, and what the employee sees.
| Problem | Our call and reason |
|---|---|
The reply says “Done, ticket 1050 is open” and tools_used is empty |
Do not trust the number: nothing ran. Look in the table and tighten the system text |
| A message says “I need a new monitor” | The model asks who is asking; nothing is created until the name arrives |
| Someone asks for the status of ticket 7777 and nobody opened it | The lookup finds no item, the model gets an error result and says there is no such ticket |
| The table cannot be reached while a ticket is being created | The function logs the cause and the model gets a safe message; the request still answers |
| You finish the chapter | Empty the bucket, delete the stack and wait until it is gone |
Flashcards
Say the answer aloud before revealing it. Speaking is a different skill from reading, so practise both.
1. What is tool use?
You describe some actions to the model, the model answers with a request to run one, your code runs it, and the result goes back to the model, which writes its reply. Like a diner and a kitchen: the diner orders, the kitchen cooks.2. Why can the model not write to the ticket table itself?
In Bedrock the model asks for a tool by name with some input. Your function decides whether to run it and does the work, so the table and the key are fixed in your code, not chosen by the model.3. What does a tool definition contain?
A name, a plain description, and an input schema: the list of details the tool needs, each with a type, and which are required. It is what the model knows about the tool, so a vague description leads to guessing.4. What does the model send when it wants a tool, and how does the answer find its way back?
It stops with the reason tool_use and sends the tool's name, the input it filled in and an id. Your result carries the same id, so the model can match each result to its request.5. What is a DynamoDB table, an item and a primary key?
A table stores data as items; an item is a group of attributes such as a title and a status; the primary key identifies each item. Here the key is TicketId, so one ticket is one item, found by its number.6. How does the helpdesk give out numbers like 1001?
One extra item named counter holds a count. Each new ticket adds one to it, and the number is 1000 plus the count. The ticket is saved if its number is unused.7. Why can the counter skip a number, and is that acceptable?
Adding to a counter is not safe to repeat, so a retried call can add twice. For a demo ticket that is fine. For money it is not, so you would design it differently.8. What is an error result, and why use one?
It is the short text your function gives the model when a tool cannot do its job, marked as an error. The model can read it and explain, so a missing detail or a failed tool does not crash the request.9. A reply names a ticket but tools_used is empty. What happened?
The model wrote the number itself and no tool ran, so nothing was saved. Treat the number as untrusted, look in the table and tighten the system text. The function writes the table.10. What does the function do when a request lacks a required detail?
The system text tells the model to ask for it. If the model calls the tool anyway, the function returns a Missing detail error and creates nothing, so the model asks the person who is asking.11. Why does the loop stop after three model calls?
Ask, read the tool result, answer takes three. If there is still no answer, the function returns an error, so a model that keeps asking for tools ends the request instead of running up the bill.12. What does chapter 5 add to the template, and how do you delete it cleanly?
Seven resources: TicketsTable, TicketsFunctionLogGroup, TicketsFunctionRole, TicketsFunction, TicketsIntegration, TicketsRoute and TicketsInvokePermission. Empty the site bucket, run delete-stack, then wait for stack-delete-complete.Model answer
The core question this chapter answers: “The helpdesk should open a ticket when someone asks for Figma access. How does that work, from the message to the ticket number in the reply?”
Four things a colleague listens for, in this order: tools described · the model asks · the function runs it · failures and the reply. Skip any of these four and the answer is incomplete.
- Tools described — the request carries the definitions of
create_ticketandget_ticket_status - The model asks — it stops with a tool request instead of an answer, and does not touch the table
- The function runs it — your code runs the tool on the table and sends the result back as one message
- Failures and the reply — a missing detail or failed tool goes back as an error result, then the model writes the reply
The full answer, said aloud:
(Tools described) “Each request to the model carries the definitions of two tools, create_ticket and get_ticket_status: a name, a description and the details each needs.
(The model asks) The model reads the message and, instead of answering, stops and asks to run create_ticket with a title and a name. It does not touch the table; it asks.
(The function runs it) My function runs the tool: it takes the next number from a counter, saves the ticket in the DynamoDB table, and sends the result back to the model as one message with the same id.
(Failures and the reply) If a detail is missing or the table fails, that goes back as an error result the model can explain, not a crash. Then the model writes the reply with the ticket number, and I check the tools_used list to know a tool really ran.“
Missing a point is a failure state. The follow-up question lands where you went quiet: finish without the model asking and the next question is who writes to the table, and finish without the failure case and the next question is what happens when the table is down. Practise until the four come out without prompting, then stop talking and let the questions come.
Why this matters
In a conversation with a colleague: a team that lets a model act is asked what it can touch and how anyone knows an action really happened. The four points answer both, and the ticket helpdesk is a specific example.
On the job: a support lead checking the ticket list, a security reviewer reading the function’s permissions, and an on-call engineer reading a failed request ask versions of the same questions. The tool list, the one-table role and the error results answer them.
In the rest of this course: chapter 6 puts these tools in a loop that works through a problem and emails the IT team, and chapter 7 adds the sign-in this endpoint is missing. A reader who knows these six parts can predict why each piece is there.
After this unit
Assignment. The chapter project asks you to deploy the stack, open tickets, read one back, trigger the missing-detail case, look at the table, and delete it. Keep the template: the next chapter extends it. The brief itself is the project file.
What’s next. The next chapter gives the helpdesk a loop, so that it checks whether a service is down, tries a known fix, and emails the IT team when it cannot settle the problem.
TechEazy Consulting training material — not affiliated with or endorsed by Amazon Web Services (AWS), Amazon Bedrock or any other company named in this course.