aie_2.5: building it (Support Assistant) A developer has published a build walkthrough for "Support Assistant," a Python script that combines structured outputs, tool use, and streaming in a single Anthropic Claude-powered customer support workflow. The script files an incoming customer message into a support log, looks up an order in a local dictionary standing in for a database, and streams the reply back to the user. Before going any further with this build, a quick note. This build assumes you have read aie 2.2, aie 2.3 and aie 2.4. If you have not, start there, it’ll make this make more sense. In the last three lessons, we followed one customer message through three jobs. Amara wrote in asking where her order was. Structured outputs turned her message into a row for the support log. Tool use let the model look up order 4821 in Kora Home’s orders table. Streaming put the answer on her screen while it was still being written. In this build, we write all three in one script. We’ll call what we build Support Assistant . What we are building A Python script that takes a customer message, files it into a support log, looks up the order in a table, and streams the answer back. It is the Kora Home assistant from the last three lessons, running as code you can send your own messages through. What you need - A terminal - Python installed on your machine - An Anthropic API key platform.claude.com https://platform.claude.com , you will need to add a small credit balance as the free tier does not cover API access - A code editor VS Code is fine if you do not have a preference Setting up If you still have the folder from the aie 1.1 build, you can work in it and skip ahead to creating the script file. Otherwise, create a folder and set up a virtual environment. A virtual environment keeps your project’s dependencies isolated, you can think of it as a container for everything this project needs, separate from anything else on your machine. mkdir ai-engineering cd ai-engineering python3 -m venv venv source venv/bin/activate You will know it worked when you see venv at the start of your terminal line. Install the two libraries you need: pip install anthropic python-dotenv - anthropic is the official Python library for talking to Claude. - python-dotenv reads your API key from a file so you never have to hardcode it in your script. Create a .env file and add your key: ANTHROPIC API KEY=your key here Create the script file: mkdir 02 touch 02/support assistant.py Open 02/support assistant.py in your editor. This is where you will build the script. Step 1: set up and add the orders data Start with the same three lines from the last build, plus one new import. python from dotenv import load dotenv import os import json from anthropic import Anthropic load dotenv client = Anthropic api key=os.getenv "ANTHROPIC API KEY" - load dotenv reads your .env file and makes everything inside it available to the script. - import os gives the script access to environment variables, the place where your API key lives after load dotenv runs. - import json is the new one. The model’s reply comes back as JSON text, and this is what turns it into something Python can read values out of. - Anthropic api key=... creates the client, the connection every call goes through. Now the orders. In a working shop this would be a database, but a database adds setup that has nothing to do with what we are learning here. A Python dictionary does the same job for this build. ORDERS = { "3310": {"customer": "Jonah", "status": "delivered", "delivery date": "2026-09-14"}, "4790": {"customer": "Amara", "status": "returned", "delivery date": "2026-09-11"}, "4821": {"customer": "Amara", "status": "delayed at courier", "delivery date": "2026-09-24"}, "5127": {"customer": "Priya", "status": "preparing", "delivery date": "2026-09-30"}, } - Each order number is a key, and the details for that order are its value. - ORDERS "4821" gives you back the dictionary with Amara’s order in it. - The capital letters are a Python convention for values that stay fixed while the script runs. And the message we are processing: customer message = "Hi, this is Amara. My order 4821 was due last week and I'm still waiting. Where is it?" Step 2: file the message with a schema Our first job is to turn the message into three clean values for the support log. We’ll start with the schema, the form the model has to fill in. record schema = { "type": "object", "properties": { "name": {"type": "string"}, "order id": {"type": "string"}, "request": {"type": "string"}, }, "required": "name", "order id", "request" , "additionalProperties": False, } - properties lists the three boxes, each one holding text. - required says all three specified fields must be filled in. - additionalProperties: False means the reply holds these three and nothing else. Now the call. It is the same client.messages.create from the last build, with one new parameter. record response = client.messages.create model="claude-haiku-4-5", max tokens=1024, messages= {"role": "user", "content": f"Turn this customer message into a support record: {customer message}"} , output config={ "format": { "type": "json schema", "schema": record schema, } }, - output config is where the schema goes. It tells the API the reply must be JSON in that exact shape. - The f before the string lets you drop customer message into the middle of it, which is called an f-string. Read the reply: record = json.loads record response.content 0 .text - record response.content 0 .text pulls the words out of the first content block, the same as the last build. - json.loads turns that JSON text into a Python dictionary, so record "order id" gives you 4821 . Print it to see what you filed: print "Filed to support log:" print f"{record 'name' } | {record 'order id' } | {record 'request' }" print Run the script: python3 02/support assistant.py You should see: Filed to support log: Amara | 4821 | order status And that’s the first job done. Step 3: describe the lookup function Job two, we want to get the model connected to the orders data. First the function that does the lookup: python def get order status order id : order = ORDERS.get order id if order is None: return {"error": "No order found with that number."} return { "order id": order id, "status": order "status" , "delivery date": order "delivery date" , } - ORDERS.get order id looks up the order number in the dictionary from Step 1. - .get returns None when the number is missing, in place of crashing, which is why the if line can catch it. - When the order exists, the function returns its status and delivery date. Try it on its own: print get order status "4821" {'order id': '4821', 'status': 'delayed at courier', 'delivery date': '2026-09-24'} That line was just to check the function works, so you can delete it before moving on. Now describe that function to the model. The model only reads text, so this is a written description of what the function does and what it needs. tools = { "name": "get order status", "description": "Look up the current status and delivery date of a Kora Home order, given its order number. Use this for any question about where an order is, when it will arrive, or whether it has shipped.", "input schema": { "type": "object", "properties": { "order id": {"type": "string", "description": "The order number, for example 4821"} }, "required": "order id" , }, } - name matches the real function so your code knows which one to run when a request comes back. - description is what the model reads to decide whether this function fits the message in front of it. The extra sentence covers the words customers actually use. - input schema is a schema again, the same kind of form from Step 2. Here it describes the inputs rather than the answer. Step 4: make the call and read the tool request Now send the message with the tool list attached. messages = {"role": "user", "content": customer message} response = client.messages.create model="claude-haiku-4-5", max tokens=1024, tools=tools, messages=messages, - messages is the conversation, starting with Amara’s message. It is a variable here because it grows in the next step. - tools is the new parameter, handing the model the description you wrote in Step 3. Check what came back: print response.stop reason tool use - end turn would mean the model finished its answer. - tool use means it stopped partway to ask for a function. The request itself is in the content blocks. In the last build content held one block of text, and this time it holds two. for block in response.content: print block.type text tool use Pull out the second one: tool request = next block for block in response.content if block.type == "tool use" print tool request.name print tool request.input get order status {'order id': '4821'} - next ... walks through the blocks and hands back the first one whose type is tool use , then stops. - tool request.name is the function the model wants. - tool request.input holds what it filled into the form, with the order number it read out of Amara’s message. Those two print lines were checks, so delete them before the next step. Step 5: run the function and send the result back The model asked for the function and your code is what runs it. order id = tool request.input "order id" print f"Looking up order {order id}..." print result = get order status order id - tool request.input "order id" pulls the order number out of the request. - get order status order id is the function from Step 3, running against the orders data. - result now holds the status and delivery date. The model forgot the first call the moment it ended, so the second call has to carry everything. Two messages go on the end of the conversation. messages.append {"role": "assistant", "content": response.content} messages.append { "role": "user", "content": { "type": "tool result", "tool use id": tool request.id, "content": json.dumps result , } , } - The first is the model’s own reply from Step 4, added back exactly as it came. This is how the model sees, on the second call, that it asked for order 4821. - The second carries the result in a tool result block. - tool use id is the label from the request, so the model knows which request this result answers. - json.dumps result is the reverse of json.loads . It turns the Python dictionary back into JSON text so it can travel inside a message. Your conversation now looks like this: 1. user Amara's message 2. assistant the model's text, plus its request for get order status 3. user the tool result holding the order details We’re done with the second job. The order details are in front of the model, and the next step is where we get the answer displayed. Step 6: stream the answer Time for the third job. The second call is where the answer comes from, so that is the one to stream. An ordinary call would look like this: final = client.messages.create model="claude-haiku-4-5", max tokens=1024, tools=tools, messages=messages, The library has a second method that takes the same parameters. Swap create for stream : with client.messages.stream model="claude-haiku-4-5", max tokens=1024, tools=tools, messages=messages, as stream: for text in stream.text stream: print text, end="", flush=True final = stream.get final message print - with keeps the connection open while the pieces arrive and closes it once the indented part finishes, whether the code ran cleanly or hit an error. - as stream names the open connection so you can reach it. - stream.text stream hands you each piece of text as it arrives, and the for loop runs once per piece. - end="" stops print adding a new line after every piece, so they join into sentences. - flush=True puts each piece on the screen immediately, in place of Python holding them back in batches. - get final message runs once the model stops writing and hands back the complete response object, the same one from the last build. The counts are on final , like before: print print f"Input tokens: {final.usage.input tokens}" print f"Output tokens: {final.usage.output tokens}" Your final code python from dotenv import load dotenv import os import json from anthropic import Anthropic load dotenv client = Anthropic api key=os.getenv "ANTHROPIC API KEY" ORDERS = { "3310": {"customer": "Jonah", "status": "delivered", "delivery date": "2026-09-14"}, "4790": {"customer": "Amara", "status": "returned", "delivery date": "2026-09-11"}, "4821": {"customer": "Amara", "status": "delayed at courier", "delivery date": "2026-09-24"}, "5127": {"customer": "Priya", "status": "preparing", "delivery date": "2026-09-30"}, } customer message = "Hi, this is Amara. My order 4821 was due last week and I'm still waiting. Where is it?" def get order status order id : order = ORDERS.get order id if order is None: return {"error": "No order found with that number."} return { "order id": order id, "status": order "status" , "delivery date": order "delivery date" , } record schema = { "type": "object", "properties": { "name": {"type": "string"}, "order id": {"type": "string"}, "request": {"type": "string"}, }, "required": "name", "order id", "request" , "additionalProperties": False, } tools = { "name": "get order status", "description": "Look up the current status and delivery date of a Kora Home order, given its order number. Use this for any question about where an order is, when it will arrive, or whether it has shipped.", "input schema": { "type": "object", "properties": { "order id": {"type": "string", "description": "The order number, for example 4821"} }, "required": "order id" , }, } record response = client.messages.create model="claude-haiku-4-5", max tokens=1024, messages= {"role": "user", "content": f"Turn this customer message into a support record: {customer message}"} , output config={ "format": { "type": "json schema", "schema": record schema, } }, record = json.loads record response.content 0 .text print "Filed to support log:" print f"{record 'name' } | {record 'order id' } | {record 'request' }" print messages = {"role": "user", "content": customer message} response = client.messages.create model="claude-haiku-4-5", max tokens=1024, tools=tools, messages=messages, tool request = next block for block in response.content if block.type == "tool use" order id = tool request.input "order id" print f"Looking up order {order id}..." print result = get order status order id messages.append {"role": "assistant", "content": response.content} messages.append { "role": "user", "content": { "type": "tool result", "tool use id": tool request.id, "content": json.dumps result , } , } with client.messages.stream model="claude-haiku-4-5", max tokens=1024, tools=tools, messages=messages, as stream: for text in stream.text stream: print text, end="", flush=True final = stream.get final message print print print f"Input tokens: {final.usage.input tokens}" print f"Output tokens: {final.usage.output tokens}" Run it python3 02/support assistant.py You should see something like this: Filed to support log: Amara | 4821 | order status Looking up order 4821... Your order 4821 is currently delayed at the courier. The updated delivery date is September 24. Sorry for the wait, let me know if there's anything else I can help with. Input tokens: 412 Output tokens: 38 The answer shows up word by word, which the we cannot simulate here but run it to see that part. What you are seeing Three calls went out in that run, and each one did a different job. The first call did not see the orders data. It read Amara’s message and filled in a form, which is why record came back with the same three fields in the same places. Change the message to something scruffier, like “hey where’s my stuff, order 4821” , and the fields come back identical. The second call is where the model asked for help. It had the orders function described to it, read 4821 out of the message, and sent back a request. The third call is where the order details reached the model in the tool result block, which is why September 24 shows up in the reply, pulled from the ORDERS dictionary. The input token count tells you something too. 412 tokens went into that last call, against the 19 you sent in the aie 1.1 build. That is Amara’s message, the model’s tool request, the order details and the tool description, all travelling together because the model forgets everything between calls. Try your own messages Change customer message and run it again. customer message = "Hi, it's Priya. Any update on 5127?" Then try one with an order number that is missing from ORDERS : customer message = "Where is order 9999?" The function will return an error that goes back to the model in the tool result block, and the model will something sensible to the customer. That is structured outputs, tool use and streaming running together. In the next lesson, aie 3.0, we look at the instructions we send the model on every call, which we have been writing without much thought so far. If you have any questions about this build, let me know