Tool calling

Lecture 21

Dr. Benjamin Soltoff

Cornell University
INFO 4940/5940 - Fall 2025

November 11, 2025

Announcements

Announcements

Learning objectives

  • Identify the value of calling external tools from LLMs
  • Define and call tools using {ellmer} and chatlas
  • Annotate tools with metadata to improve user experience
  • Implement tools that interact with Shiny apps

Application exercise

ae-20

Instructions

  • Go to the course GitHub org and find your ae-20 (repo name will be suffixed with your GitHub name).
  • Clone the repo in Positron, install required packages using renv::restore() (R) or uv sync (Python), open the Quarto document in the repo, and follow along and complete the exercises.
  • Render, commit, and push your edits by the AE deadline – end of the day

Recall: How do LLMs work?

  1. You write some words

  2. The LLM writes some more words

  3. You use those words

On their own, can LLMs… access the internet? send an email? interact with the world?

Let’s try it

library(ellmer)

chat <- chat("openai/gpt-4.1-nano")

chat$chat("What's the weather like in Ithaca, NY?")
chat$chat("What day is it?")
import chatlas

chat = chatlas.Chat(model="openai/gpt-4.1-nano")
chat.chat("What's the weather like in Ithaca, NY?")
chat.chat("What day is it?")

Tools

a.k.a. functions, tool calling or function calling

  • Bring real-time or up-to-date information to the model

  • Let the model interact with the world

Chatbot Systems

How do tool calls work?

What should I wear to campus tomorrow?

Human in the loop

👨‍💻 _demos/18_manual-tools/app.R

Wait… I can write code!

library(ellmer)

ellmer::create_tool_def(weathR::point_forecast, verbose = TRUE)
tool(
  weathR::point_forecast,
  name = "point_forecast",
  description = "Retrieve point forecast meteorological data for a given latitude, longitude, and 
timezone.",
  arguments = list(
    lat = type_number("Latitude of the location for the forecast."),
    lon = type_number("Longitude of the location for the forecast."),
    timezone = type_string("The nominal timezone for the forecast. One of OlsonNames(), or '-1' for
local time. Defaults to '-1'.", required = FALSE),
    dir_numeric = type_boolean("TRUE for numeric wind directions, FALSE for character wind 
directions. Defaults to FALSE.", required = FALSE)
  )
)

Wait… I can write code!

get_weather <- tool(
  \(lat, lon) weathR::point_forecast(lat, lon),
  name = "point_forecast",
  description = "Get forecast data for a specific latitude and longitude.",
  arguments = list(
    lat = type_number("Latitude of the location."),
    lon = type_number("Longitude of the location.")
  )
)

Wait… I can write code!

get_weather(lat = 42.4397, lon = -76.4953)
Simple feature collection with 156 features and 8 fields
Geometry type: POINT
Dimension:     XY
Bounding box:  xmin: -76.4953 ymin: 42.4397 xmax: -76.4953 ymax: 42.4397
Geodetic CRS:  WGS 84
First 10 features:
                      time temp dewpoint humidity p_rain wind_speed wind_dir               skies
1  2025-11-12 12:00:00 EST   43 1.666667       73     35         10       SW Chance Rain Showers
2  2025-11-12 13:00:00 EST   44 2.222222       73     42         10       SW Chance Rain Showers
3  2025-11-12 14:00:00 EST   44 3.333333       79     44         10       SW Chance Rain Showers
4  2025-11-12 15:00:00 EST   44 3.333333       79     56         10        W Rain Showers Likely
5  2025-11-12 16:00:00 EST   45 3.888889       79     42         10        W Chance Rain Showers
6  2025-11-12 17:00:00 EST   44 3.888889       82     33          9       SW Chance Rain Showers
7  2025-11-12 18:00:00 EST   43 3.888889       86     36          8       SW Chance Rain Showers
8  2025-11-12 19:00:00 EST   42 3.888889       89     37          7       SW Chance Rain Showers
9  2025-11-12 20:00:00 EST   40 3.333333       92     40          7        W Chance Rain Showers
10 2025-11-12 21:00:00 EST   40 3.333333       92     33          8        W Chance Rain Showers
                   geometry
1  POINT (-76.4953 42.4397)
2  POINT (-76.4953 42.4397)
3  POINT (-76.4953 42.4397)
4  POINT (-76.4953 42.4397)
5  POINT (-76.4953 42.4397)
6  POINT (-76.4953 42.4397)
7  POINT (-76.4953 42.4397)
8  POINT (-76.4953 42.4397)
9  POINT (-76.4953 42.4397)
10 POINT (-76.4953 42.4397)

Wait… I can write code!

chat <- chat_openai(model = "gpt-4.1-nano", echo = "output")

# Register the tool with the chatbot
chat$register_tool(get_weather)

chat$chat("What should I wear to class tomorrow in Ithaca, NY?")
◯ [tool call] point_forecast(lat = 42.4534, lon = -76.4862)
#> [{"time":"2025-11-10 11:00:00 EST","temp":33,"dewpoint":-1.1111,"humidity":89,"p_rain":57,"wind_speed":…
#> The weather forecast for tomorrow in Ithaca, NY indicates cold temperatures around the low 30s to high 20s 
#> Fahrenheit, with a high chance of snow showers and rain showers throughout the day. It will be windy with wind 
#> speeds around 8-16 mph.
#> 
#> Given these conditions, I recommend wearing warm and waterproof clothing. A good outfit would include:
#> - A warm, insulated waterproof coat
#> - Layers like thermal shirts and sweaters
#> - Waterproof gloves and a hat
#> - Waterproof boots to keep your feet dry and warm
#> - An umbrella might also be useful in case of rain
#> 
#> Make sure to stay warm and dry while attending your class!

Recap: Tool definitions in R

tool_get_weather <- tool(
  tool_fn,
  description = "How and when to use the tool",
  arguments = list(
    .... = type_string(),
    .... = type_integer(),
    .... = type_enum(
      c("choice1", "choice2"),
      required = FALSE
    )
  )
)

Recap: Tool definitions in Python

def get_weather(lat, lon):
    return NWS.GetCurrentForecast(lat, lon)

Recap: Tool definitions in Python

def get_weather(lat: float, lon: float):
    return NWS.GetCurrentForecast(lat, lon)

Recap: Tool definitions in Python

def get_weather(lat: float, lon: float):
    """
    Get forecast data for a specific latitude and longitude.

    Parameters
    ----------
    lat : str
        Latitude of the location.
    lon : str
        Longitude of the location.
    """
    return NWS.GetCurrentForecast(lat, lon)

Recap: Registering tools in Python

import chatlas

chat = chatlas.ChatAuto("openai/gpt-4.1-nano")
chat.register_tool(get_weather)

chat.chat("What should I wear to class tomorrow in Ithaca, NY?")

⌨️ 20_quiz-game-2

Instructions

  1. I’ve given you a function that plays a sound when called.

  2. Your job: teach the model to play sounds in the Quiz Show game we made last week.

06:00

Tool Annotations

  • Provide hints to clients about tool behavior

  • Help humans and AI systems make informed decisions about tool usage

  • All properties are hints only - not guaranteed to be accurate

  • MCP Tool Annotations Spec

Key Properties

Property Description
title Human-readable tool name
readOnlyHint Tool doesn’t modify environment
destructiveHint May perform destructive updates
idempotentHint Repeated calls with same arguments return same
openWorldHint Interacts with external entities vs. closed domain

Tool Annotations in R

tool_get_weather <- tool(
  get_weather,
  # description, arguments, ...
  annotations = tool_annotations(
    title = "Get Weather",
    readOnlyHint = TRUE,
    icon = fontawesome::fa_i("cloud-sun")
  )
)

Tool Annotations in Python

chat.register_tool(
    get_weather,
    annotations={
        "title": "Get Weather",
        "readOnlyHint": True,
        "extra": {
            "icon": faicons.fa_i("cloud-sun"),
        },
    },
)

⌨️ 21_quiz-game-3

Instructions

  1. Add tool annotations to our play_sound tools.

  2. Pick an icon from FontAwesome free icons

08:00

What if we want to decide title and icon dynamically in the tool?

ellmer::ContentToolResult

get_weather <- function(lat, lon) {
  weathR::point_forecast(lat, lon)
}

ellmer::ContentToolResult

get_weather <- function(lat, lon) {
  forecast <- weathR::point_forecast(lat, lon)

  ContentToolResult(
    forecast
  )
}

ellmer::ContentToolResult

get_weather <- function(lat, lon) {
  forecast <- weathR::point_forecast(lat, lon)

  ContentToolResult(
    forecast,
    extra = list(
      display = list(
        title = paste("Weather for", lat, lon),
        icon = fontawesome::fa_i("cloud-sun")
      )
    )
  )
}

ellmer::ContentToolResult

get_weather <- function(lat, lon, location_name) {
  forecast <- weathR::point_forecast(lat, lon)

  ContentToolResult(
    forecast,
    extra = list(
      display = list(
        title = paste("Weather for", location_name),
        icon = fontawesome::fa_i("cloud-sun")
      )
    )
  )
}

chatlas.ContentToolResult

def get_weather(lat: float, lon: float, location_name: str):
    forecast = NWS.GetCurrentForecast(lat, lon)

    return ContentToolResult(
        forecast,
        extra={
            "display": {
                "title": f"Weather for {location_name}",
                "icon": faicons.fa_i("cloud-sun"),
            }
        },
    )

⌨️ 22_quiz-game-4

Instructions

  1. Pick an icon and a title for each action in the Quiz show app.

  2. Return a ContentToolResult from each tool that sets the icon and title.

  3. Play the game again to see the icons and titles in action.

08:00

Tools in Shiny

  • Define the tool function inside the server

  • You can update reactive values in the tool function!

  • You can read reactive values, if you isolate() reads. (Be careful!)

⌨️ 23_quiz-game-5

Instructions

I’ve updated the app to show the score in value boxes.
(And turned off the sounds.)

  1. In the system prompt, tell the model when to use update_score.

  2. Read update_score(). It should update a reactive value in the app when called. Finish implementing it.

  3. Complete the tool definition.

  4. Add tool annotations (optional, but fun!)

08:00

Wrap-up

Recap

  • Tool calling lets LLMs access real-time information and interact with the world
  • We can define tools in R and Python using {ellmer} and chatlas
  • Tool annotations improve the user experience by providing hints about tool behavior
  • Tools can interact with Shiny apps to update reactive values

Acknowledgments