06 Chatbot With Multiple Tools
Tool use with multiple tools
Now we're going to take our tool use to another level! We're going to provide Claude with a suite of tools it can select from. Our goal is to build a simple customer support chatbot for a fictional electronics company called TechNova. It will have access to simple tools including:
get_userto look up user details by email, username, or phone numberget_order_by_idto look up an order directly by its order IDget_customer_ordersto look up all the orders belonging to a customercancel_ordercancels an order given a specific order ID
This chatbot will be quite limited, but it demonstrates some key pieces of working with multiple tools.
Here's an image showcasing an example interaction with the chatbot.
Here's another interaction with the chatbot where we've explicitly printed out a message any time Claude uses a tool, to make it easier to understand what's happening behind the scenes:
Here's a diagram showing the flow of information for a potential single chat message:
important note: this chatbot is extremely simple and allows anyone to cancel an order as long as they have the username or email of the user who placed the order. This implementation is for educational purposes and should not be integrated into a real codebase without alteration!
Our fake database
Before we start working with Claude, we'll begin by defining a very simple FakeDatabase class that will hold some fake customers and orders, as well as providing methods for us to interact with the data. In the real world, the chatbot would presumably be connected to one or more actual databases.
We'll create an instance of the FakeDatabase:
Let's make sure our get_user method works as intended. It's expecting us to pass in a key that is one of:
- "email"
- "username"
- "phone"
It also expects a corresponding value that it will use to perform a basic search, hopefully returning the matching user.
{'id': '1213210',
, 'name': 'John Doe',
, 'email': 'john@gmail.com',
, 'phone': '123-456-7890',
, 'username': 'johndoe'} {'id': '4782901',
, 'name': 'Aaliyah Davis',
, 'email': 'aaliyahd@hotmail.com',
, 'phone': '111-222-3333',
, 'username': 'adavis'} {'id': '8259147',
, 'name': 'Megan Anderson',
, 'email': 'megana@gmail.com',
, 'phone': '666-777-8888',
, 'username': 'manderson'} We can also get a list of all orders that belong to a particular customer by calling get_customer_orders and passing in a customer ID:
[{'id': '24601',
, 'customer_id': '1213210',
, 'product': 'Wireless Headphones',
, 'quantity': 1,
, 'price': 79.99,
, 'status': 'Shipped'},
, {'id': '13579',
, 'customer_id': '1213210',
, 'product': 'Smartphone Case',
, 'quantity': 2,
, 'price': 19.99,
, 'status': 'Processing'},
, {'id': '90357',
, 'customer_id': '1213210',
, 'product': 'Smartphone Case',
, 'quantity': 1,
, 'price': 19.99,
, 'status': 'Shipped'}] [{'id': '61984',
, 'customer_id': '9603481',
, 'product': 'Noise-Cancelling Headphones',
, 'quantity': 1,
, 'price': 149.99,
, 'status': 'Shipped'}] We also can look up a single order directly if we have the order ID:
{'id': '24601',
, 'customer_id': '1213210',
, 'product': 'Wireless Headphones',
, 'quantity': 1,
, 'price': 79.99,
, 'status': 'Shipped'} Lastly, we can use the cancel_order method to cancel an order. Orders can only be cancelled if they are "processing". We can't cancel an order that has already shipped! The method expects us to pass in an order_id of the order we want to cancel.
{'id': '47652',
, 'customer_id': '8259147',
, 'product': 'Smartwatch',
, 'quantity': 1,
, 'price': 199.99,
, 'status': 'Processing'} 'Cancelled the order'
{'id': '47652',
, 'customer_id': '8259147',
, 'product': 'Smartwatch',
, 'quantity': 1,
, 'price': 199.99,
, 'status': 'Cancelled'} Writing our tools
Now that we've tested our (very) simple fake database functionality, let's write JSON schemas that define the structure of the tools. Let's take a look at a very simple tool that corresponds to our get_order_by_id method:
As always, we include a name, a description, and an overview of the inputs. In this case, there is a single input, order_id, and it is required.
Now let's take a look at a more complex tool that corresponds to the get_user method. Recall that this method has two arguments:
- A
keyargument that is one of the following strings:- "email"
- "username"
- "phone"
- A
valueargument which is the term we'll be using to search (the actual email, phone number, or username)
Here's the corresponding tool definition:
Pay special attention to the way we define the set of possible valid options for key using enum in the schema.
We still have two more tools to write, but to save time, we'll just provide you the complete list of tools ready for us to use. (You're welcome to try defining them youself first as an exercise!)
Giving our tools to Claude
Next up, we need to do a few things:
- Tell Claude about our tools and send the user's chat message to Claude.
- Handle the response we get back from Claude:
- If Claude doesn't want to use a tool:
- Print Claude's output to the user.
- Ask the user for their next message.
- If Claude does want to use a tool
- Verify that Claude called one of the appropriate tools.
- Execute the underlying function, like
get_userorcancel_order. - Send Claude the result of running the tool.
- If Claude doesn't want to use a tool:
Eventually, the goal is to write a command line script that will create an interactive chatbot that will run in a loop. But to keep things simple, we'll start by writing the basic code linearly. We'll end by creating an interactive chatbot script.
We start with a function that can translate Claude's tool use response into an actual method call on our db:
Let's start with a simple demo that shows Claude can decide which tool to use when presented with a list of multiple tools. We'll ask Claude Can you look up my orders? My email is john@gmail.com and see what happens!
ToolsBetaMessage(id='msg_0112Ab7iKF2gWeAcAd2XWaBb', content=[TextBlock(text='Sure, let me look up your orders based on your email. To get the user details by email:', type='text'), ToolUseBlock(id='toolu_01CNTeufcesL1sUP2jnwT8TA', input={'key': 'email', 'value': 'john@gmail.com'}, name='get_user', type='tool_use')], model='claude-3-sonnet-20240229', role='assistant', stop_reason='tool_use', stop_sequence=None, type='message', usage=Usage(input_tokens=574, output_tokens=96))
Claude wants to use the get_user tool.
Now we'll write the basic logic to update our messages list, call the appropriate tool, and update our messages list again with the tool results.
Claude wants to use the {tool_name} tool
Tool Input:
{
"key": "email",
"value": "john@gmail.com"
}
Tool Result:
{
"id": "1213210",
"name": "John Doe",
"email": "john@gmail.com",
"phone": "123-456-7890",
"username": "johndoe"
}
This is what our messages list looks like now:
[{'role': 'user',
, 'content': 'Can you look up my orders? My email is john@gmail.com'},
, {'role': 'assistant',
, 'content': [TextBlock(text='Sure, let me look up your orders based on your email. To get the user details by email:', type='text'),
, ToolUseBlock(id='toolu_01CNTeufcesL1sUP2jnwT8TA', input={'key': 'email', 'value': 'john@gmail.com'}, name='get_user', type='tool_use')]},
, {'role': 'user',
, 'content': [{'type': 'tool_result',
, 'tool_use_id': 'toolu_01CNTeufcesL1sUP2jnwT8TA',
, 'content': "{'id': '1213210', 'name': 'John Doe', 'email': 'john@gmail.com', 'phone': '123-456-7890', 'username': 'johndoe'}"}]}] Now we'll send a second request to Claude using the updated messages list:
ToolsBetaMessage(id='msg_011Fru6wiViEExPXg7ANQkfH', content=[TextBlock(text='Now that I have your customer ID 1213210, I can retrieve your order history:', type='text'), ToolUseBlock(id='toolu_011eHUmUgCZXwr7swzL1LE6y', input={'customer_id': '1213210'}, name='get_customer_orders', type='tool_use')], model='claude-3-sonnet-20240229', role='assistant', stop_reason='tool_use', stop_sequence=None, type='message', usage=Usage(input_tokens=733, output_tokens=80)) Now that Claude has the result of the get_user tool, including the user's ID, it wants to call get_customer_orders to look up the orders that correspond to this particular customer. It's picking the right tool for the job. Note: there are many potential issues and places Claude could go wrong here, but this is a start!
Writing an interactive script
It's a lot easier to interact with this chatbot via an interactive command line script.
Here's all of the code from above combined into a single function that starts a loop-based chat session (you'll probably want to run this as its own script from the command line):
Here's an example conversation:
As you can see, the chatbot is calling the correct tools when needed, but the actual chat responses are not ideal. We probably don't want a customer-facing chatbot telling users about the specific tools it's going to call!
Prompt enhancements
We can improve the chat experience by providing our chatbot with a solid system prompt. One of the first things we should do is give Claude some context and tell it that it's acting as a customer service assistant for TechNova.
Here's a start for our system prompt:
Don't forget to pass this prompt into the system parameter when making requests to Claude!
Here's a sample conversation after adding in the above system prompt details:
Notice that the assistant knows it is a support bot for TechNova, and it no longer tells the user about its tools. It might also be worth explicitly telling Claude not to reference tools at all.
Note: The =======Claude wants to use the cancel_order_tool======= lines are logging we added to the script, not Claude's actual outputs!
If you play with this script enough, you'll likely notice other issues. One very obvious and very problematic issue is illustrated by the following exchange:
The screenshot above shows the entire conversation. The user asks for help canceling an order and states that they don't know the order ID. Claude should follow up and ask for the customer's email, phone number, or username to look up the customer and then find the matching orders. Instead, Claude just decides to call the get_user tool without actually knowing any information about the user. Claude made up an email address and tried to use it, but of course didn't find a matching customer.
To prevent this, we'll update the system prompt to include language along the lines of:
Here's a screenshot of a conversation with the assistant that uses the updated system prompt:
Much better!
An Opus-specific problem
When working with Opus and tools, the model often outputs its thoughts in <thinking> or <reflection> tags before actually responding to a user. You can see an example of this in the screenshot below:
This thinking process tends to lead to better results, but it obviously makes for a terrible user experience. We don't want to expose that thinking logic to a user! The easiest fix here is to explicitly ask the model output its user-facing response in a particular set of XML tags. We start by updating the system prompt:
Take a look at an example conversation output:
Claude is now responding with <reply> tags around the actual user-facing response. All we need to do now is extract the content between those tags and make sure that's the only part of the response we actually print to the user. Here's an updated start_chat function that does exactly that!
Here's a screenshot showing the impact of the above change:
Final version
Here's a screenshot of a longer conversation with a version of this script that colorizes the output.
Closing notes
This is an educational demo made to illustrate the tool use workflow. This script and prompt are NOT ready for production. The assistant responds with things like "You can find the order ID on the order confirmation email" without actually knowing if that's true. In the real world, we would need to provide Claude with lots of background knowledge about our particular company (at a bare minimum). We also need to rigorously test the chatbot and make sure it behaves well in all situations. Also, it's probably a bad idea to make it so easy for anyone to cancel an order! It would be pretty easy to cancel a lot of people's orders if you had access to their email, username, or phone number. In the real world, we would probably want some form of authentication. Long story short: this is a demo and not a complete chatbot implementation!
Exercise
- Add functionality to our assistant (and underlying
FakeDatabaseclass) that allows a user to update their email and phone number. - Define a new tool called
get_user_infothat combines the functionality ofget_userandget_customer_orders. This tool should take a user's email, phone, or username as input and return the user's information along with their order history all in one go. - Add error handling and input validation!
Bonus
- Update the chatbot to use an ACTUAL database instead of our
FakeDatabaseclass.