Case studies and industry use cases

Easy tool calls with ellmer and chatlas

Sara Altman
Written by Sara Altman
2023-01-13
An image shows two hexagonal logos below a row of four icons connected by an arrow. The top icons, from left to right, are: a cloud, a laptop with a bar graph, a stack of cylinders representing a database, and a gear with "API" inside. The arrow points from these icons to the two logos below. The logo on the left is a white hexagon with a black outline, featuring a white silhouette of Atlas holding up a sphere with chat bubbles and data visualizations. Below it, the word "CHATLAS" is written in white. The l

Want an LLM to help you schedule your week? Large language models (LLMs) are trained on vast amounts of public information, but lack access to many external or private data sources or systems, like your calendar. If you ask an LLM what your schedule looks like next week, it will respond that it doesn’t know because it doesn’t have access to your schedule.

import chatlas

chat = chatlas.ChatAnthropic(system_prompt="You are a helpful personal assistant.")
chat.chat("What's my schedule like next week?")
I don't have access to your personal calendar or schedule. I can't see your actual 
appointments or commitments. To know your schedule for next week, you would need to
check your own calendar, planner, or scheduling system.   
library(ellmer)

chat <- chat_claude(system_prompt = "You are a helpful personal assistant.")
chat$chat("What's my schedule like next week?")
I don't have access to your personal schedule. To help you with your schedule for 
next week, I would need you to share that information with me. 

You could tell me about any appointments you already have planned, or ask me for 
suggestions on how to organize your upcoming week effectively.

If the LLM could access our calendar, however, it could easily reason about it and answer our scheduling questions. We can use tool calling to grant the LLM that access. Creating a tool with ellmer and chatlas is relatively straightfoward. If you can write a function in R or Python, you can write a tool.

We will use ellmer and chatlas to interact with LLMs. If you’re unfamiliar with the basics of ellmer, chatlas, or working with LLM APIs, check out the packages websites and these resources:

What is tool calling?

Tool calling lets an LLM request that specific code be run when it needs to access data or perform actions outside its own capabilities. It works like this:

  1. You ask the LLM to answer a question, do a task, etc.

  2. The model realizes it needs a function (a “tool”) to fulfill the request.

  3. The model asks the host code to run that function.

  4. The function runs, returns data, and the model uses that data in its response.

Tip

For more information about tool-calling, see the tool calling articles on the ellmer and chatlas websites.

Importantly, the model itself does not execute code. The model only requests that the caller (in our case, R or Python) run the tool with particular arguments. The intelligence of the model is used to 1) know when it is appropriate to request the tool be called and 2) select reasonable arguments for the tool. This makes the model agentic because it can decide what to do, when, and how, by choosing and invoking tools as needed.

ellmer and chatlas make tool calling in R and Python easy. In other frameworks, you may need to define tools using a JSON schema. ellmer and chatlas let you skip that step by letting you write native R or Python functions directly and handling the translation to and from JSON behind the scenes.

Why use tool calling?

Tool calling gives the LLM capabilities it wouldn’t otherwise have access to, including:

  • Access to APIs, databases, files, etc.
  • The ability to use up-to-date or user-specific information.
  • More intelligent or specific responses to user requests, beyond the model’s typical abilities.

Implement tool calling in ellmer or chatlas

To implement tool calling with ellmer or chatlas, you need three components:

  1. A function (or tool) that implements your desired action.

  2. A chat object created with ellmer or chatlas.

  3. Tool registration so the model knows how to use your function.

While not required, you will also likely want to write a custom system prompt instructing the model when and how to use your tool.

Calendar API example

Now, let’s see it in action in our calendar example.

Note

To keep the example focused, we’ve created helper functions for authenticating to the Google Calendar API and querying the API. We also created a tool that can get the current date so the user can ask questions like, “What’s my schedule like today?” You can see the full scripts here: R, Python.

1. Define the Tool

First, create a function that carries out the action you want the LLM to have access to. This should just be an ordinary R or Python function.

def get_calendar_events(start_date: str, end_date: str) -> List[Dict[str, str]]:
    """
    Fetch Google Calendar events between two dates.

    Parameters
    ----------
    start_date : str
        The start date in "YYYY-MM-DD" format.
    end_date : str
        The end date in "YYYY-MM-DD" format.

    Returns
    -------
    List[dict]
        A list of events with 'start' and 'summary' fields.
    """
    creds = authenticate()
    service = build_calendar_service(creds)
    return query_api(service, start_date, end_date)

In R, use roxygen2 comments to document the function, just like you would for a function in an R package.

#' Gets the calendar events between start_date and end_date
#'
#' @param start_date Start date, in YYYY-MM-DD format.
#' @param end_date End date, in YYYY-MM-DD format.
#' @return A tibble containing the calendar events in the date range. 
get_calendar_events <- function(start_date, end_date) {

  token <- calendar_authenticate()

  events <- query_api(start_date, end_date, token)

  if (length(events) == 0) { return(tibble()) }

  tibble(
    id = events$id,
    summary = events$summary,
    start = events$start$dateTime,
    end =  events$end$dateTime
  )
}

The process is largely the same as writing a function for a human user. When thinking about the functions inputs and outputs, you’ll need to consider:

  • What information does the LLM have access to? Make sure the function’s inputs are elements you want the LLM to control and that it has the capability to provide.
  • What information do you want the LLM to have after the tool call completes? The function should return this information.

For example, in our case, we want the LLM to be able to retrieve calendar information. Based on the user’s request, the LLM can provide dates as inputs, and the function will return a list (Python) or tibble (R) of calendar events.

Because the LLM communicates through JSON, inputs and outputs must use JSON-compatible data types, like floats, lists, or simple data frames. Do not return complex objects, functions, environments, etc. Keep in mind that returning large data frames can consume many tokens, as each cell contributes to the token count.

You can learn more about type limitations here for ellmer and here for chatlas.

Note

If you want to implement the calendar tool yourself, you will need to generate your own credentials.json and (if using R) update the email address used. You can learn how to generate the JSON file here.

2. Initialize a chat

Create a chat with ellmer or chatlas using your desired chat function. You can see a list of available models and their corresponding functions for ellmer here and for chatlas here.

It is also helpful to supply a system prompt that specifies when and how to use the tool, as well as example conversations and tool calls. In our calendar examples, we supply this markdown file containing our prompt.

Tip

You can learn more about prompt design here.

chat = chatlas.ChatAnthropic(system_prompt=open("prompt-calendar.md", "r").read())
chat <- chat_claude(system_prompt = readLines("prompt-calendar.md"))

3. Register the tool

Finally, registering the tool allows the LLM to request the tool be called.

To register the tool, use chat.register_tool() in Python or chat$register_tool() and tool() in R.

chat.register_tool(get_calendar_events)

In R, use tool()’s required .description argument to describe what the function does. To provide information about the function arguments, you can also provide name-value pairs that define the arguments and their types accepted by the function. You can see the available type specifications here. Use required = TRUE if that argument is required.

chat$register_tool(tool(
  get_calendar_events,
  .description = "Fetches calendar events between a specified start and end date.",
  start_date = type_string(
       "The start date from which to fetch calendar events. It should be a string representing the 
date in the format YYYY-MM-DD.",
       required = TRUE
  ),
  end_date = type_string(
       "The end date up to which to fetch calendar events. It should be a string representing the 
date in the format YYYY-MM-DD.",
       required = TRUE
  )
))

Start chatting

The final step is to either send an initial message to the model or start up an interactive conversation in the console (chat.console() in Python; live_console(chat) in R).

Below, you can see our calendar tool in action.

Below is all the code put together. You can also see the scripts here: R, Python.

# 1. Define and document the tool

def get_calendar_events(start_date: str, end_date: str) -> List[Dict[str, str]]:
    """
    Fetch Google Calendar events between two dates.

    Parameters
    ----------
    start_date : str
        The start date in "YYYY-MM-DD" format.
    end_date : str
        The end date in "YYYY-MM-DD" format.

    Returns
    -------
    List[dict]
        A list of events with 'start' and 'summary' fields.
    """
    creds = authenticate()
    service = build_calendar_service(creds)
    return query_api(service, start_date, end_date)

# 2. Initialize a chat

chat = chatlas.ChatAnthropic(system_prompt=open("prompt-calendar.md", "r").read())

# 3. Register the tool

chat.register_tool(get_calendar_events)

#  Start the chat console 
chat.console()
# 1. Define and document the tool

#' Gets the calendar events between start_date and end_date
#'
#' @param start_date Start date, in YYYY-MM-DD format.
#' @param end_date End date, in YYYY-MM-DD format.
#' @return A tibble containing the calendar events in the date range. 
get_calendar_events <- function(start_date, end_date) {

  token <- calendar_authenticate()

  events <- query_api(start_date, end_date, token)

  if (length(events) == 0) { return(tibble()) }

  tibble(
    id = events$id,
    summary = events$summary,
    start = events$start$dateTime,
    end =  events$end$dateTime
  )
}

# 2. Initialize a chat

chat <- chat_claude(system_prompt = readLines("prompt-calendar.md"))

# 3. Register the tool

chat$register_tool(tool(
  get_calendar_events,
  "Fetches calendar events between a specified start and end date.",
  start_date = type_string(
       "The start date from which to fetch calendar events. It should be a string representing the 
date in the format YYYY-MM-DD.",
       required = TRUE
  ),
  end_date = type_string(
       "The end date up to which to fetch calendar events. It should be a string representing the 
date in the format YYYY-MM-DD.",
       required = TRUE
  )
))

#  Start the chat console 
live_console(chat)

In a Shiny app

Tool calling can be especially useful in interactive contexts like Shiny apps. You might want the LLM to generate a plot, filter a table, update a value box, or take other actions in your app. Without tool calling, the LLM can’t do any of that.

Implementing tool calling in a Shiny app is very similar to implementing tool calling outside a Shiny app. Include the tool code and tool registration code in the server function.

Tip

To learn more about creating Shiny apps that interact with LLMs, see shinychat for R and the Generative AI section of the Shiny for Python website for Python.

Tips and tricks

Simple tool calls are easy to get started with, but getting the LLM to reliably use them the way you want can be tricky, especially as your tools grow more complex. Here are a few tips to help you build and debug tool calls more effectively.

  1. Build incrementally. Start by just writing and testing the core function, without involving the LLM. Once it works, register the tool, write a system prompt, and test in a simple chat console interface. Then, if you want, try embedding the tool in a Shiny app. Working step-by-step makes it easier to pinpoint issues.

  2. Debug using model responses. If something goes wrong, you can ask the model what happened. It has access to the full error message returned by your tool function, which can help you diagnose the issue without even leaving the chat.

  3. Write a thorough system prompt. Registering the tool provides some information about how the tool should work, but including additional information in the system prompt makes it more likely that the LLM will actually call the tool how and when you want. See the sidebot system prompt for an example of a thorough, well-structured system prompt. You may also need to refine or “debug” your system prompt if the LLM isn’t using your tool as intended.

  4. Verify that the tool is actually being called. Sometimes, the model might simulate a response without actually triggering the tool. If the results seem off or the data seems fabricated, check that the tool function is actually being called. You may need to revise your system prompt to clarify when and how the LLM should initiate a tool call.

  5. Chain multiple tools. LLMs can call more than one tool per turn. In our example, the LLM first calls get_date() to fetch the current date, and then passes that information to get_calendar_events(), all in one turn.

  6. Take advantage of automatic retries. If a tool call fails, ellmer or chatlas will return the error to the model, and in many cases the model will attempt to recover and try again with new arguments.

Debugging

Debugging tool calls is a bit different than regular debugging, since issues can come from multiple areas:

  1. The function itself. Your function might have “traditional” bugs that throw an error or produce unexpected results.
  2. The tool registration. Make sure you’re registering the correct function, and that the argument types are specified correctly.
  3. The system prompt. The model needs clear instructions about when and how to use your tool.

To help you diagnose problems, chatlas and ellmer allow you to echo tool calls and their results. This shows how and when the model requested a tool call, what was returned, and any error message the tool produced.

This can also be useful if you want to expose the tool call and results to the user.

Use echo="all" in chat.chat() to view tool calls, their results, and any error messages.

def get_date():
    "Gets the current date."
    raise ValueError("Error in tool function.")

chat = chatlas.ChatAnthropic()
chat.register_tool(get_date)

chat.chat("What date is it today?", echo="all")
👤 User turn:                                                   
                                                                                                                  
What date is it today?                                                                                                                                                                                                                                       
🤖 Assistant turn:                                                
                                                                                                                  
I'll try to help you find out today's date again.<< 🤖 other content >>                                           
                                                                                                                  
                                                                                                                  
 # tool request (toolu_0184fJdYdvo5C2ibvHAC4aQi)                                                                  
 get_date()                                                                                                       
                                                                                                                  
                                                                                                                  
<< 🤖 finish reason: tool_use >>                                                                                  
                                                                                                 
                                                                                                                  
👤 User turn:                                                   
                                                                                                                  
                                                                                                                  
 # tool result (toolu_0184fJdYdvo5C2ibvHAC4aQi)                                                                   
 Tool calling failed with error: 'Error in tool function.'                                                        
                                                                                                                  
🤖 Assistant turn:                                                
                                                                                                                  
I apologize for the continued technical difficulties with the date function. If you'd like me to try a different approach to help you, please let me know.                                     

<< 🤖 finish reason: end_turn >>                                                                                  

<chatlas._chat.ChatResponse at 0x11e67d1e0>

Use echo = "output" in chat() to echo the tool request and response.

get_date <- function() {
  Sys.Date()
}

chat <- chat_anthropic(model = "claude-3-7-sonnet-latest")

chat$register_tool(tool(get_date, "Gets the current date."))

chat$chat("What date is it today?", echo = "output")
I can check the current date for you.
◯ [tool call] get_date()
● #> "2025-04-29"
Today's date is April 29, 2025.

And if something goes wrong:

get_date <- function() {
  # Always throws an error
  stop("Error in tool function.")
}

chat <- chat_anthropic(model = "claude-3-7-sonnet-latest")

chat$register_tool(tool(get_date, "Gets the current date."))

chat$chat("What date is it today?", echo = "output")
I can check the current date for you.
◯ [tool call] get_date()
■ #> Error: Error in tool function.
I apologize for the error. It seems there was an issue with the date tool. The current date would be the date on 
your device or system. Since I don't have direct access to your local system information, I can't provide the 
exact date on your end.

If you need the current date, you can check it on your device's calendar, clock, or by looking at the date 
display on your computer or mobile device.

Conclusion

Tool calling lets you extend what an LLM can do by connecting it to your own functions. With ellmer and chatlas, it’s easy to get started. To learn more, see the tool calling vignettes for ellmer and chatlas.

Sara Altman

Sara Altman

Sara is a Data Science Educator on the Developer Relations team at Posit.