AE 20: Tool calling
Suggested answers
Application exercise
Answers
R
Python
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
- Materials derived in part from Programming with LLMs and licensed under a Creative Commons Attribution 4.0 International (CC BY) License.