Notebooks
A
Anthropic
06 Chatbot With Multiple Tools

06 Chatbot With Multiple Tools

tool_useanthropic-courses

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_user to look up user details by email, username, or phone number
  • get_order_by_id to look up an order directly by its order ID
  • get_customer_orders to look up all the orders belonging to a customer
  • cancel_order cancels 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.

conversation1.png

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:

conversation2.png

Here's a diagram showing the flow of information for a potential single chat message:

chat_diagram.png

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.

[1]

We'll create an instance of the FakeDatabase:

[2]

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.

[3]
{'id': '1213210',
, 'name': 'John Doe',
, 'email': 'john@gmail.com',
, 'phone': '123-456-7890',
, 'username': 'johndoe'}
[4]
{'id': '4782901',
, 'name': 'Aaliyah Davis',
, 'email': 'aaliyahd@hotmail.com',
, 'phone': '111-222-3333',
, 'username': 'adavis'}
[5]
{'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:

[6]
[{'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'}]
[7]
[{'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:

[8]
{'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.

[9]
{'id': '47652',
, 'customer_id': '8259147',
, 'product': 'Smartwatch',
, 'quantity': 1,
, 'price': 199.99,
, 'status': 'Processing'}
[10]
'Cancelled the order'
[11]
{'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:

[12]

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:

  1. A key argument that is one of the following strings:
    • "email"
    • "username"
    • "phone"
  2. A value argument which is the term we'll be using to search (the actual email, phone number, or username)

Here's the corresponding tool definition:

[13]

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!)

[14]

Giving our tools to Claude

Next up, we need to do a few things:

  1. Tell Claude about our tools and send the user's chat message to Claude.
  2. 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_user or cancel_order.
      • Send Claude the result of running the 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:

[15]

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!

[16]
[17]
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.

[18]
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:

[19]
[{'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:

[20]
[21]
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):

[23]

Here's an example conversation:

conversation3.png

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:

[24]

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:

conversation4.png

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:

conversation5.png

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:

[26]

Here's a screenshot of a conversation with the assistant that uses the updated system prompt:

conversation6.png

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:

conversation7.png

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:

[27]

Take a look at an example conversation output:

conversation8.png

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:

conversation9.png


Final version

Here's a screenshot of a longer conversation with a version of this script that colorizes the output.

conversation10.png


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 FakeDatabase class) that allows a user to update their email and phone number.
  • Define a new tool called get_user_info that combines the functionality of get_user and get_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 FakeDatabase class.