AE 20: Tool calling

Suggested answers

Application exercise
Answers
R
Python
Modified

November 11, 2025

20_quiz-game-2

library(shiny)
library(bslib)
library(beepr)
library(ellmer)
library(shinychat)

# Tools ------------------------------------------------------------------------

#' Plays a sound effect.
#'
#' @param sound Which sound effect to play: `"correct"`, `"incorrect"`,
#'   `"new-round"`, or `"you-win"`.
#' @returns A confirmation that the sound was played.
play_sound <- function(
  sound = c("correct", "incorrect", "new-round", "you-win")
) {
  sound <- match.arg(sound)

  switch(
    sound,
    correct = beepr::beep("coin"),
    incorrect = beepr::beep("wilhelm"),
    "new-round" = beepr::beep("fanfare"),
    "you-win" = beepr::beep("mario")
  )

  glue::glue("The '{sound}' sound was played.")
}

tool_play_sound <- tool(
  play_sound,
  description = "Play a sound effect",
  arguments = list(
    sound = type_enum(
      c("correct", "incorrect", "new-round", "you-win"),
      description = paste(
        "Which sound effect to play.",
        "Play 'new-round' after the user picks a theme for the round.",
        "Play 'correct' or 'incorrect' after the user answers a question.",
        "Play 'you-win' at the end of a round of questions."
      )
    )
  )
)

# UI ---------------------------------------------------------------------------

ui <- page_fillable(
  chat_mod_ui("chat")
)

# Server -----------------------------------------------------------------------

server <- function(input, output, session) {
  client <- chat(
    "anthropic/claude-3-7-sonnet-20250219",
    system_prompt = interpolate_file(
      here::here("data/prompt.md")
    )
  )

  client$register_tool(tool_play_sound)

  chat <- chat_mod_server("chat", client)

  observe({
    # Start the game when the app launches
    chat$update_user_input(
      value = "Let's play the quiz game!",
      submit = TRUE
    )
  })
}

shinyApp(ui, server)
from pathlib import Path
from typing import Literal

import chatlas
import dotenv
from playsound3 import playsound
from pyhere import here
from shiny import App, reactive, ui

dotenv.load_dotenv()

# Tools ------------------------------------------------------------------------
SoundChoice = Literal["correct", "incorrect", "new-round", "you-win"]

sound_map: dict[SoundChoice, Path] = {
    "correct": here("data/sounds/smb_coin.wav"),
    "incorrect": here("data/sounds/wilhelm.wav"),
    "new-round": here("data/sounds/victory_fanfare_mono.wav"),
    "you-win": here("data/sounds/smb_stage_clear.wav"),
}


def play_sound(sound: SoundChoice = "correct") -> str:
    """
    Plays a sound effect.

    Parameters
    ----------
    sound: Which sound effect to play: "correct", "incorrect", "new-round" or
           "you-win". Play the "new-round" sound after the user picks a theme
           for the round. Play the "correct" and "incorrect" sounds when the
           user answers a question correctly or incorrectly, respectively. And
           play the "you-win" sound at the end of a round of questions.

    Returns
    -------
    A confirmation that the sound was played.
    """
    if sound not in sound_map.keys():
        raise ValueError(
            f"sound must be one of {sorted(sound_map.keys())}; got {sound!r}"
        )

    playsound(sound_map[sound])

    return f"The '{sound}' sound was played."


# UI ---------------------------------------------------------------------------

app_ui = ui.page_fillable(
    ui.chat_ui("chat"),
)


def server(input, output, session):
    chat_ui = ui.Chat(id="chat")

    # Set up the chat instance
    client = chatlas.ChatAnthropic(
        model="claude-3-7-sonnet-20250219",
        system_prompt=here("data/prompt.md").read_text(),
    )
    client.register_tool(play_sound)

    @chat_ui.on_user_submit
    async def handle_user_input(user_input: str):
        # Use `content="all"` to include tool calls in the response stream
        response = await client.stream_async(user_input, content="all")
        await chat_ui.append_message_stream(response)

    @reactive.effect
    def _():
        # Start the game when the app launches
        chat_ui.update_user_input(value="Let's play the quiz game!", submit=True)


app = App(app_ui, server)

21_quiz-game-3

library(shiny)
library(bslib)
library(beepr)
library(ellmer)
library(shinychat)

# Tools ------------------------------------------------------------------------

#' Plays a sound effect.
#'
#' @param sound Which sound effect to play: `"correct"`, `"incorrect"`,
#'   `"new-round"`, or `"you-win"`.
#' @returns A confirmation that the sound was played.
play_sound <- function(
  sound = c("correct", "incorrect", "new-round", "you-win")
) {
  sound <- match.arg(sound)

  switch(
    sound,
    correct = beepr::beep("coin"),
    incorrect = beepr::beep("wilhelm"),
    "new-round" = beepr::beep("fanfare"),
    "you-win" = beepr::beep("mario")
  )

  glue::glue("The '{sound}' sound was played.")
}

tool_play_sound <- tool(
  play_sound,
  description = "Play a sound effect",
  arguments = list(
    sound = type_enum(
      c("correct", "incorrect", "new-round", "you-win"),
      description = paste(
        "Which sound effect to play.",
        "Play 'new-round' after the user picks a theme for the round.",
        "Play 'correct' or 'incorrect' after the user answers a question.",
        "Play 'you-win' at the end of a round of questions."
      )
    )
  ),
  annotations = tool_annotations(
    title = "Play Sound Effect",
    icon = fontawesome::fa_i("volume-high")
  )
)


# UI ---------------------------------------------------------------------------

ui <- page_fillable(
  chat_mod_ui("chat")
)

# Server -----------------------------------------------------------------------

server <- function(input, output, session) {
  client <- chat(
    "anthropic/claude-3-7-sonnet-20250219",
    system_prompt = interpolate_file(
      here::here("data/prompt.md")
    )
  )

  client$register_tool(tool_play_sound)

  chat <- chat_mod_server("chat", client)

  observe({
    # Start the game when the app launches
    chat$update_user_input(
      value = "Let's play the quiz game!",
      submit = TRUE
    )
  })
}

shinyApp(ui, server)
from pathlib import Path
from typing import Literal

import chatlas
import dotenv
import faicons
from playsound3 import playsound
from pyhere import here
from shiny import App, reactive, ui

dotenv.load_dotenv()

# Tools ------------------------------------------------------------------------
SoundChoice = Literal["correct", "incorrect", "new-round", "you-win"]

sound_map: dict[SoundChoice, Path] = {
    "correct": here("data/sounds/smb_coin.wav"),
    "incorrect": here("data/sounds/wilhelm.wav"),
    "new-round": here("data/sounds/victory_fanfare_mono.wav"),
    "you-win": here("data/sounds/smb_stage_clear.wav"),
}


def play_sound(sound: SoundChoice = "correct") -> str:
    """
    Plays a sound effect.

    Parameters
    ----------
    sound: Which sound effect to play: "correct", "incorrect", "new-round" or
           "you-win". Play the "new-round" sound after the user picks a theme
           for the round. Play the "correct" and "incorrect" sounds when the
           user answers a question correctly or incorrectly, respectively. And
           play the "you-win" sound at the end of a round of questions.

    Returns
    -------
    A confirmation that the sound was played.
    """
    if sound not in sound_map.keys():
        raise ValueError(
            f"sound must be one of {sorted(sound_map.keys())}; got {sound!r}"
        )

    playsound(sound_map[sound])

    return f"The '{sound}' sound was played."


# UI ---------------------------------------------------------------------------

app_ui = ui.page_fillable(
    ui.chat_ui("chat"),
)


def server(input, output, session):
    chat_ui = ui.Chat(id="chat")

    # Set up the chat instance
    client = chatlas.ChatAnthropic(
        model="claude-3-7-sonnet-20250219",
        system_prompt=here("data/prompt.md").read_text(),
    )
    client.register_tool(
        play_sound,
        annotations={
            "title": "Play Sound Effect",
            "extra": {
                "icon": faicons.icon_svg("volume-high"),
            },
        },
    )

    @chat_ui.on_user_submit
    async def handle_user_input(user_input: str):
        # Use `content="all"` to include tool calls in the response stream
        response = await client.stream_async(user_input, content="all")
        await chat_ui.append_message_stream(response)

    @reactive.effect
    def _():
        # Start the game when the app launches
        chat_ui.update_user_input(value="Let's play the quiz game!", submit=True)


app = App(app_ui, server)

22_quiz-game-4

library(shiny)
library(bslib)
library(beepr)
library(ellmer)
library(shinychat)

# Tools ------------------------------------------------------------------------

#' Plays a sound effect.
#'
#' @param sound Which sound effect to play: `"correct"`, `"incorrect"`,
#'   `"new-round"`, or `"you-win"`.
#' @returns A confirmation that the sound was played.
play_sound <- function(
  sound = c("correct", "incorrect", "new-round", "you-win")
) {
  sound <- match.arg(sound)

  switch(
    sound,
    correct = beepr::beep("coin"),
    incorrect = beepr::beep("wilhelm"),
    "new-round" = beepr::beep("fanfare"),
    "you-win" = beepr::beep("mario")
  )

  icon <- switch(
    sound,
    correct = fontawesome::fa_i(
      "circle-check",
      class = "text-success",
      prefer_type = "solid"
    ),
    incorrect = fontawesome::fa_i(
      "circle-xmark",
      class = "text-danger",
      prefer_type = "solid"
    ),
    "new-round" = fontawesome::fa_i(
      "circle-play",
      class = "text-primary",
      prefer_type = "solid"
    ),
    "you-win" = fontawesome::fa_i("medal", class = "text-warning")
  )

  title <- switch(
    sound,
    correct = "That's right!",
    incorrect = "Oops, not quite.",
    "new-round" = "Let's goooooo!",
    "you-win" = "You Win!"
  )

  ContentToolResult(
    glue::glue("The '{sound}' sound was played."),
    extra = list(
      display = list(
        title = title,
        icon = icon
      )
    )
  )
}

tool_play_sound <- tool(
  play_sound,
  description = "Play a sound effect",
  arguments = list(
    sound = type_enum(
      c("correct", "incorrect", "new-round", "you-win"),
      description = paste(
        "Which sound effect to play.",
        "Play 'new-round' after the user picks a theme for the round.",
        "Play 'correct' or 'incorrect' after the user answers a question.",
        "Play 'you-win' at the end of a round of questions."
      )
    )
  ),
  annotations = tool_annotations(title = "Play Sound Effect")
)


# UI ---------------------------------------------------------------------------

ui <- page_fillable(
  chat_mod_ui("chat")
)

# Server -----------------------------------------------------------------------

server <- function(input, output, session) {
  client <- chat(
    "anthropic/claude-3-7-sonnet-20250219",
    system_prompt = interpolate_file(
      here::here("data/prompt.md")
    )
  )

  client$register_tool(tool_play_sound)

  chat <- chat_mod_server("chat", client)

  observe({
    # Note: This block starts the game when the app launches
    chat$update_user_input(
      value = "Let's play the quiz game!",
      submit = TRUE
    )
  })
}

shinyApp(ui, server)
from pathlib import Path
from typing import Any, Literal

import chatlas
import dotenv
from faicons import icon_svg
from playsound3 import playsound
from pyhere import here
from shiny import App, reactive, ui

dotenv.load_dotenv()

# Tools ------------------------------------------------------------------------
SoundChoice = Literal["correct", "incorrect", "new-round", "you-win"]

sound_map: dict[SoundChoice, Path] = {
    "correct": here("data/sounds/smb_coin.wav"),
    "incorrect": here("data/sounds/wilhelm.wav"),
    "new-round": here("data/sounds/victory_fanfare_mono.wav"),
    "you-win": here("data/sounds/smb_stage_clear.wav"),
}

icon_map: dict[SoundChoice, Any] = {
    "correct": icon_svg("circle-check", fill="var(--bs-success)"),
    "incorrect": icon_svg("circle-xmark", fill="var(--bs-danger)"),
    "new-round": icon_svg("circle-play", fill="var(--bs-primary)"),
    "you-win": icon_svg("trophy", fill="var(--bs-warning)"),
}

title_map: dict[SoundChoice, str] = {
    "correct": "That's right!",
    "incorrect": "Oops, not quite.",
    "new-round": "Let's goooooooo!",
    "you-win": "You Win",
}


def play_sound(sound: SoundChoice = "correct") -> str:
    """
    Plays a sound effect.

    Parameters
    ----------
    sound: Which sound effect to play: "correct", "incorrect", "new-round" or
           "you-win". Play the "new-round" sound after the user picks a theme
           for the round. Play the "correct" and "incorrect" sounds when the
           user answers a question correctly or incorrectly, respectively. And
           play the "you-win" sound at the end of a round of questions.

    Returns
    -------
    A confirmation that the sound was played.
    """
    if sound not in sound_map.keys():
        raise ValueError(
            f"sound must be one of {sorted(sound_map.keys())}; got {sound!r}"
        )

    playsound(sound_map[sound])

    return chatlas.ContentToolResult(
        value=f"The '{sound}' sound was played.",
        extra={
            "display": {
                "title": title_map[sound],
                "icon": icon_map[sound],
            }
        },
    )


# UI ---------------------------------------------------------------------------

app_ui = ui.page_fillable(
    ui.chat_ui("chat"),
)


def server(input, output, session):
    # Recall: We set up the Chat UI server logic and the chat client in the
    # server function so that each user session gets its own chat history.
    chat_ui = ui.Chat(id="chat")
    client = chatlas.ChatAnthropic(
        model="claude-3-7-sonnet-20250219",
        # Use your quiz game system prompt, or switch to _solutions to use ours
        system_prompt=here("data/prompt.md").read_text(),
    )

    client.register_tool(
        play_sound,
        annotations={"title": "Play Sound Effect"},
    )

    @chat_ui.on_user_submit
    async def handle_user_input(user_input: str):
        response = await client.stream_async(user_input, content="all")
        await chat_ui.append_message_stream(response)

    @reactive.effect
    def _():
        # Note: This block starts the game when the app launches
        chat_ui.update_user_input(value="Let's play the quiz game!", submit=True)


app = App(app_ui, server)

23_quiz-game-5

library(shiny)
library(bslib)
library(ellmer)
library(shinychat)

# UI ---------------------------------------------------------------------------

ui <- page_sidebar(
  title = "Quiz Game 5",
  sidebar = sidebar(
    position = "right",
    fillable = TRUE,
    width = 400,
    value_box(
      "Correct Answers",
      textOutput("txt_correct"),
      showcase = fontawesome::fa_i("circle-check"),
      theme = "success"
    ),
    value_box(
      "Incorrect Answers",
      textOutput("txt_incorrect"),
      showcase = fontawesome::fa_i("circle-xmark"),
      theme = "danger"
    )
  ),
  navset_card_tab(
    nav_panel("Quiz Game", chat_mod_ui("chat")),
    nav_panel("Your Answers", tableOutput("tbl_scores"))
  )
)

# Server -----------------------------------------------------------------------

server <- function(input, output, session) {
  client <- chat(
    "anthropic/claude-3-7-sonnet-20250219",
    system_prompt = interpolate_file(
      here::here("data/prompt.md")
    ) |>
      paste(
        "\n\nAfter every question, use the 'Update Score' tool to keep track of the user's score.",
        "Be sure to call the tool after you have graded the user's final answer to the question."
      )
  )

  output$tbl_scores <- renderTable(scores())
  output$txt_correct <- renderText(sum(scores()$is_correct, na.rm = TRUE))
  output$txt_incorrect <- renderText(sum(!scores()$is_correct, na.rm = TRUE))

  scores <- reactiveVal(
    data.frame(
      theme = character(),
      question = character(),
      answer = character(),
      your_answer = character(),
      is_correct = logical()
    )
  )

  update_score <- function(theme, question, answer, your_answer, is_correct) {
    the_scores <- isolate(scores())

    new_score <- data.frame(
      theme = theme,
      question = question,
      answer = answer,
      your_answer = your_answer,
      is_correct = is_correct
    ) # fmt: skip
    the_scores <- rbind(the_scores, new_score)
    scores(the_scores)

    correct <- sum(the_scores$answer == the_scores$your_answer)
    list(correct = correct, incorrect = nrow(the_scores) - correct)
  }

  client$register_tool(tool(
    update_score,
    description = paste(
      "Add a correct or incorrect answer to the score tally.",
      "Call this tool after you've graded the user's answer to a question."
    ),
    arguments = list(
      theme = type_string("The theme of the round."),
      question = type_string("The quiz question that was asked."),
      answer = type_string("The correct answer to the question."),
      your_answer = type_string("The user's answer to the question."),
      is_correct = type_boolean("Whether the user's answer was correct.")
    ),
    annotations = tool_annotations(
      title = "Update Score",
      icon = fontawesome::fa_i("circle-plus")
    )
  ))

  chat <- chat_mod_server("chat", client)

  observe({
    # Note: This block starts the game when the app launches
    chat$update_user_input(
      value = "Let's play the quiz game!",
      submit = TRUE
    )
  })
}

shinyApp(ui, server)
from typing import TypedDict

import chatlas
import dotenv
import polars as pl
from faicons import icon_svg
from pyhere import here
from shiny import App, reactive, render, ui

dotenv.load_dotenv()

# UI ---------------------------------------------------------------------------

app_ui = ui.page_sidebar(
    ui.sidebar(
        ui.value_box(
            "Correct Answers",
            ui.output_text("txt_correct"),
            showcase=icon_svg("circle-check"),
            theme="success",
        ),
        ui.value_box(
            "Incorrect Answers",
            ui.output_text("txt_incorrect"),
            showcase=icon_svg("circle-xmark"),
            theme="danger",
        ),
        position="right",
        fillable=True,
        width=400,
    ),
    ui.navset_card_underline(
        ui.nav_panel(
            "Quiz Game",
            ui.chat_ui("chat"),
        ),
        ui.nav_panel(
            "Your Answers",
            ui.output_data_frame("tbl_score"),
        ),
    ),
    title="Quiz Game 5",
    fillable=True,
)


class QuestionAnswer(TypedDict):
    theme: str
    question: str
    answer: str
    your_answer: str
    is_correct: bool


def server(input, output, session):
    chat_ui = ui.Chat(id="chat")

    # Set up the chat instance
    client = chatlas.ChatAnthropic(
        model="claude-3-7-sonnet-20250219",
        system_prompt=f"""
{here("data/prompt.md").read_text()}

After every question, use the "Update Score" tool to keep track of the user's
score. Be sure to call the tool after you have graded the user's final answer to
the question.
""",
    )

    scores = reactive.value[list[QuestionAnswer]]([])

    @render.data_frame
    def tbl_score():
        df = pl.DataFrame(scores())
        return df

    @render.text
    def txt_correct() -> int:
        return len([d for d in scores() if d["is_correct"]])

    @render.text
    def txt_incorrect() -> int:
        return len([d for d in scores() if not d["is_correct"]])

    def update_score(
        theme: str,
        question: str,
        answer: str,
        your_answer: str,
        is_correct: bool,
    ):
        """
        Add a correct or incorrect answer to the score. Call this tool after
        you've graded the user's answer to a question.

        Parameters
        ----------
        theme: The theme of the round.
        question: The quiz question that was asked.
        answer: The correct answer to the question.
        your_answer: The user's answer to the question.
        is_correct: Whether the user's answer was correct.
        """
        with reactive.isolate():
            val_scores = scores.get()

        answer = QuestionAnswer(
            theme=theme,
            question=question,
            answer=answer,
            your_answer=your_answer,
            is_correct=is_correct,
        )

        val_scores = [*val_scores, answer]
        scores.set(val_scores)

        correct = len([d for d in val_scores if d["is_correct"]])
        incorrect = len(val_scores) - correct
        return {"correct": correct, "incorrect": incorrect}

    client.register_tool(
        update_score,
        annotations={
            "title": "Update Score",
            "extra": {"icon": icon_svg("circle-plus")},
        },
    )

    @chat_ui.on_user_submit
    async def handle_user_input(user_input: str):
        response = await client.stream_async(user_input, content="all")
        await chat_ui.append_message_stream(response)

    @reactive.effect
    def _():
        chat_ui.update_user_input(value="Let's play the quiz game!", submit=True)


app = App(app_ui, server)

Acknowledgments