Skip to contents

Introduction

Custom tools are the foundation of building intelligent AI agents with Claude. They allow you to extend Claude’s capabilities by giving it access to your own R functions—enabling Claude to query databases, call APIs, perform calculations, manipulate files, and interact with any system you can access from R.

This vignette provides a comprehensive deep dive into custom tools, covering everything from fundamental concepts to advanced agent architectures. By the end, you’ll understand how to build sophisticated AI-powered workflows that combine Claude’s reasoning with your domain-specific functionality.

How Tool Use Works: The Agentic Loop

Before diving into implementation, it’s essential to understand the mechanics of tool use at the API level.

The Request-Response Cycle

When you send a message to Claude with tools registered, the following cycle occurs:

  1. Initial Request: Your message and tool definitions are sent to Claude
  2. Claude’s Decision: Claude analyzes the request and decides whether tools are needed
  3. Tool Call: If tools are needed, Claude returns a tool_use response with the function name and arguments
  4. Execution: Your code (via ellmer) executes the tool and captures the result
  5. Tool Result: The result is sent back to Claude as a tool_result message
  6. Iteration: Claude may call more tools or provide a final response
  7. Completion: Claude synthesizes all information into a final answer

This cycle can repeat multiple times—Claude might call several tools sequentially, or even call the same tool multiple times with different arguments.

┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│   User      │ ──▶│   Claude    │────▶│   Tool      │
│   Message   │     │   Decides   │     │   Executes  │
└─────────────┘     └─────────────┘     └─────────────┘
                           │                   │
                           │◀──────────────────┘
                           │    Tool Result
                           ▼
                    ┌─────────────┐
                    │   Final     │
                    │   Response  │
                    └─────────────┘

Tool Definitions: The Contract

Each tool you define creates a contract with Claude. This contract includes:

  • Name: A unique identifier (snake_case recommended)
  • Description: What the tool does and when to use it
  • Parameters: The inputs the tool accepts, with types and descriptions

Claude uses this contract to decide when to call your tool and how to format the arguments. A well-written contract leads to accurate tool usage; a vague one leads to errors and hallucinations.

Creating Your First Tool

Let’s start with a complete example that demonstrates the core concepts.

The claude_tool() Function

library(artclaude)

# Define a tool that retrieves artist information
get_artist_info <- claude_tool(
  fn = function(artist_name) {
    # Simulate database lookup
    artists <- list(
      "Van Gogh" = list(
        full_name = "Vincent Willem van Gogh",
        birth_year = 1853,
        death_year = 1890,
        nationality = "Dutch",
        movement = "Post-Impressionism",
        famous_works = c("Starry Night", "Sunflowers", "Bedroom in Arles")
      ),
      "Monet" = list(
        full_name = "Claude Monet",
        birth_year = 1840,
        death_year = 1926,
        nationality = "French",
        movement = "Impressionism",
        famous_works = c("Water Lilies", "Impression, Sunrise", "Haystacks")
      )
    )

    # Find matching artist (case-insensitive partial match)
    match <- names(artists)[grepl(artist_name, names(artists), ignore.case = TRUE)]

    if (length(match) == 0) {
      return(paste("No artist found matching:", artist_name))
    }

    info <- artists[[match[1]]]
    paste(
      sprintf("**%s** (%d-%d)", info$full_name, info$birth_year, info$death_year),
      sprintf("Nationality: %s", info$nationality),
      sprintf("Movement: %s", info$movement),
      sprintf("Famous works: %s", paste(info$famous_works, collapse = ", ")),
      sep = "\n"
    )
  },
  name = "get_artist_info",
  desc = "Retrieve biographical information about a famous artist. Use this when the user asks about an artist's life, works, or historical context.",
  artist_name = ellmer::type_string("The name of the artist to look up")
)

Let’s break down each component:

The Function (fn)

The function is your R code that executes when Claude calls the tool. Key considerations:

  • Return type: Return a character string for best results. Claude will incorporate this text into its response.
  • Error handling: Handle errors gracefully and return informative messages rather than throwing exceptions.
  • Side effects: Be cautious with side effects (file writes, API calls, database modifications). Claude may call tools multiple times.

The Name (name)

Choose names that are:

  • Descriptive of the action: get_artist_info, search_database, send_notification
  • Written in snake_case
  • Unique across all registered tools

The Description (desc)

This is arguably the most important part. Claude uses this to decide when to call your tool. Write descriptions that:

  • Explain what the tool does
  • Specify when it should be used
  • Mention what it should NOT be used for (if disambiguation is needed)
# Good description
desc <- "Search the artwork database by title, artist, or style. Returns matching artworks with metadata. Use this when the user wants to find specific artworks or browse the collection. Do NOT use for artist biographical information—use get_artist_info instead."

# Too vague
desc <- "Search stuff"

# Missing context
desc <- "Queries the art_pieces table"

The Parameters

Parameters define the inputs your function accepts. Use ellmer’s type functions to specify:

# String parameter
artist_name <- ellmer::type_string("The artist's name to search for")

# Integer parameter
limit <- ellmer::type_integer("Maximum number of results to return")

# Number (float) parameter
min_price <- ellmer::type_number("Minimum price in USD")

# Boolean parameter
include_sold <- ellmer::type_boolean("Whether to include already-sold items")

# Enum (fixed choices)
sort_by <- ellmer::type_enum(
  c("date", "price", "relevance"),
  "How to sort results"
)

Parameter Types Deep Dive

Understanding parameter types is crucial for building robust tools.

Basic Types

# String: for text input
ellmer::type_string("Description of what this string represents")

# Integer: for whole numbers
ellmer::type_integer("Description of the integer")

# Number: for decimals/floats
ellmer::type_number("Description of the number")

# Boolean: for true/false
ellmer::type_boolean("Description of the flag")

Enum: Constrained Choices

Use enums when only specific values are valid:

category_tool <- claude_tool(
  fn = function(category) {
    sprintf("Searching in category: %s", category)
  },
  name = "search_by_category",
  desc = "Search artworks by category",
  category = ellmer::type_enum(
    values = c("painting", "sculpture", "photography", "digital", "mixed_media"),
    description = "The artwork category to search"
  )
)

Claude will only provide values from the specified list, preventing invalid inputs.

Arrays: Multiple Values

When your function accepts lists of items:

batch_lookup_tool <- claude_tool(
  fn = function(ids) {
    results <- lapply(ids, function(id) {
      sprintf("Result for %s: [data]", id)
    })
    paste(unlist(results), collapse = "\n")
  },
  name = "batch_lookup",
  desc = "Look up multiple items by their IDs in a single request",
  ids = ellmer::type_array(
    items = ellmer::type_string(),
    description = "List of item IDs to look up"
  )
)

Objects: Structured Input

For complex inputs with multiple fields:

create_artwork_tool <- claude_tool(
  fn = function(artwork) {
    sprintf(
      "Created artwork: '%s' by %s (%d) - %s",
      artwork$title,
      artwork$artist,
      artwork$year,
      artwork$medium
    )
  },
  name = "create_artwork_record",
  desc = "Create a new artwork record in the database",
  artwork = ellmer::type_object(
    title = ellmer::type_string("Title of the artwork"),
    artist = ellmer::type_string("Name of the artist"),
    year = ellmer::type_integer("Year the artwork was created"),
    medium = ellmer::type_string("Medium/materials used")
  )
)

Nested Structures

Combine types for complex schemas:

exhibition_tool <- claude_tool(
  fn = function(exhibition) {
    n_works <- length(exhibition$artworks)
    sprintf(
      "Exhibition '%s' at %s: %d artworks",
      exhibition$title,
      exhibition$venue,
      n_works
    )
  },
  name = "create_exhibition",
  desc = "Create a new exhibition with multiple artworks",
  exhibition = ellmer::type_object(
    title = ellmer::type_string("Exhibition title"),
    venue = ellmer::type_string("Venue name"),
    start_date = ellmer::type_string("Start date (YYYY-MM-DD)"),
    end_date = ellmer::type_string("End date (YYYY-MM-DD)"),
    artworks = ellmer::type_array(
      items = ellmer::type_object(
        artwork_id = ellmer::type_string("Artwork ID"),
        display_order = ellmer::type_integer("Order in exhibition")
      ),
      description = "List of artworks in the exhibition"
    )
  )
)

Using Tools with claude_new()

Once you’ve defined tools, register them with your chat session:

# Register tools at creation
chat <- claude_new(
  tools = list(get_artist_info, search_artworks),
  temp = 0 # Lower temperature for more consistent tool usage
)

# Claude will automatically use tools when appropriate
chat$chat("Tell me about Vincent van Gogh and his most famous paintings")

Dynamic Tool Registration

Add tools to an existing session:

chat <- claude_new()

# Start a conversation
chat$chat("Let's explore some art history")

# Add tools later
chat$register_tool(get_artist_info)
chat$register_tool(search_artworks)

# Now Claude can use them
chat$chat("What can you tell me about Monet?")

Case Study: Building an Art Collection Assistant

Let’s build a comprehensive example that demonstrates advanced tool usage patterns.

Scenario

We’re building an AI assistant for an art gallery that can:

  1. Search the artwork inventory
  2. Look up artist information
  3. Check artwork availability
  4. Calculate pricing with discounts
  5. Create reservation holds

Tool Definitions

library(artclaude)

# Simulated database
.gallery_db <- new.env()
.gallery_db$artworks <- data.table::data.table(
  id = c("ART001", "ART002", "ART003", "ART004", "ART005"),
  title = c("Sunset Harbor", "Urban Dreams", "Forest Light", "Abstract Motion", "Coastal Serenity"),
  artist = c("Maria Chen", "James Walker", "Maria Chen", "Sofia Rodriguez", "James Walker"),
  year = c(2021, 2022, 2020, 2023, 2021),
  medium = c("Oil on canvas", "Acrylic", "Watercolor", "Mixed media", "Oil on canvas"),
  price = c(4500, 3200, 2800, 5500, 3800),
  status = c("available", "available", "sold", "available", "on_hold")
)

# Tool 1: Search artworks
search_gallery <- claude_tool(
  fn = function(query = NULL, artist = NULL, medium = NULL,
                min_price = NULL, max_price = NULL, status = NULL) {
    dt <- .gallery_db$artworks

    if (!is.null(query)) {
      dt <- dt[grepl(query, title, ignore.case = TRUE) |
        grepl(query, artist, ignore.case = TRUE)]
    }
    if (!is.null(artist)) {
      dt <- dt[grepl(artist, artist, ignore.case = TRUE)]
    }
    if (!is.null(medium)) {
      dt <- dt[grepl(medium, medium, ignore.case = TRUE)]
    }
    if (!is.null(min_price)) {
      dt <- dt[price >= min_price]
    }
    if (!is.null(max_price)) {
      dt <- dt[price <= max_price]
    }
    if (!is.null(status)) {
      dt <- dt[status == !!status]
    }

    if (nrow(dt) == 0) {
      return("No artworks found matching your criteria.")
    }

    results <- apply(dt, 1, function(row) {
      sprintf(
        "**%s** (ID: %s)\n  Artist: %s | Year: %s | Medium: %s\n  Price: $%s | Status: %s",
        row["title"], row["id"], row["artist"], row["year"],
        row["medium"], format(as.numeric(row["price"]), big.mark = ","),
        row["status"]
      )
    })

    paste(c(sprintf("Found %d artwork(s):\n", nrow(dt)), results), collapse = "\n\n")
  },
  name = "search_gallery",
  desc = "Search the gallery's artwork inventory. Use this to find artworks by title, artist, medium, price range, or availability status. Returns detailed information about matching artworks.",
  query = ellmer::type_string("General search term for title or artist"),
  artist = ellmer::type_string("Filter by artist name"),
  medium = ellmer::type_string("Filter by medium (e.g., 'oil', 'watercolor', 'acrylic')"),
  min_price = ellmer::type_number("Minimum price in USD"),
  max_price = ellmer::type_number("Maximum price in USD"),
  status = ellmer::type_enum(
    c("available", "sold", "on_hold"),
    "Filter by availability status"
  )
)

# Tool 2: Get artwork details
get_artwork_details <- claude_tool(
  fn = function(artwork_id) {
    dt <- .gallery_db$artworks[id == artwork_id]

    if (nrow(dt) == 0) {
      return(sprintf("Artwork with ID '%s' not found.", artwork_id))
    }

    row <- as.list(dt[1])
    sprintf(
      "**%s** (ID: %s)\n\nArtist: %s\nYear: %s\nMedium: %s\nPrice: $%s\nStatus: %s\n\nDimensions: 24\" x 36\"\nFramed: Yes\nCertificate of Authenticity: Included",
      row$title, row$id, row$artist, row$year, row$medium,
      format(row$price, big.mark = ","), row$status
    )
  },
  name = "get_artwork_details",
  desc = "Get complete details about a specific artwork by its ID. Use this when you have an artwork ID and need full information including dimensions and framing.",
  artwork_id = ellmer::type_string("The artwork ID (e.g., 'ART001')")
)

# Tool 3: Calculate price with discount
calculate_price <- claude_tool(
  fn = function(artwork_id, discount_code = NULL) {
    dt <- .gallery_db$artworks[id == artwork_id]

    if (nrow(dt) == 0) {
      return(sprintf("Artwork with ID '%s' not found.", artwork_id))
    }

    base_price <- dt$price[1]
    discount_pct <- 0
    discount_name <- "None"

    if (!is.null(discount_code)) {
      discounts <- list(
        "MEMBER10" = list(pct = 0.10, name = "Member Discount"),
        "FIRST20" = list(pct = 0.20, name = "First Purchase"),
        "GALLERY15" = list(pct = 0.15, name = "Gallery Event")
      )

      if (discount_code %in% names(discounts)) {
        discount_pct <- discounts[[discount_code]]$pct
        discount_name <- discounts[[discount_code]]$name
      } else {
        return(sprintf("Invalid discount code: '%s'", discount_code))
      }
    }

    discount_amount <- base_price * discount_pct
    final_price <- base_price - discount_amount
    tax <- final_price * 0.0875 # 8.75% tax
    total <- final_price + tax

    sprintf(
      "**Price Calculation for %s**\n\nBase Price: $%s\nDiscount (%s): -$%s\nSubtotal: $%s\nTax (8.75%%): $%s\n\n**Total: $%s**",
      dt$title[1],
      format(base_price, big.mark = ",", nsmall = 2),
      discount_name,
      format(discount_amount, big.mark = ",", nsmall = 2),
      format(final_price, big.mark = ",", nsmall = 2),
      format(tax, big.mark = ",", nsmall = 2),
      format(total, big.mark = ",", nsmall = 2)
    )
  },
  name = "calculate_price",
  desc = "Calculate the total price for an artwork including any applicable discounts and tax. Use this when a customer wants to know the final price or apply a discount code.",
  artwork_id = ellmer::type_string("The artwork ID"),
  discount_code = ellmer::type_string("Optional discount code (e.g., 'MEMBER10', 'FIRST20', 'GALLERY15')")
)

# Tool 4: Place hold on artwork
place_hold <- claude_tool(
  fn = function(artwork_id, customer_name, customer_email, hold_days = 3) {
    dt <- .gallery_db$artworks[id == artwork_id]

    if (nrow(dt) == 0) {
      return(sprintf("Artwork with ID '%s' not found.", artwork_id))
    }

    if (dt$status[1] != "available") {
      return(sprintf(
        "Cannot place hold on '%s' - current status is '%s'.",
        dt$title[1], dt$status[1]
      ))
    }

    # Update status (in real app, this would be a database update)
    .gallery_db$artworks[id == artwork_id, status := "on_hold"]

    hold_until <- Sys.Date() + hold_days
    confirmation <- sprintf("HOLD-%s-%s", artwork_id, format(Sys.time(), "%Y%m%d%H%M"))

    sprintf(
      "**Hold Confirmed**\n\nConfirmation #: %s\nArtwork: %s (ID: %s)\nCustomer: %s (%s)\nHold Until: %s\n\nThe artwork has been reserved. The customer will be contacted to complete the purchase within %d days.",
      confirmation, dt$title[1], artwork_id, customer_name, customer_email,
      format(hold_until, "%B %d, %Y"), hold_days
    )
  },
  name = "place_hold",
  desc = "Place a temporary hold on an artwork for a customer. Use this when a customer wants to reserve an artwork before completing the purchase. Requires customer name and email.",
  artwork_id = ellmer::type_string("The artwork ID to place on hold"),
  customer_name = ellmer::type_string("Customer's full name"),
  customer_email = ellmer::type_string("Customer's email address"),
  hold_days = ellmer::type_integer("Number of days to hold (default: 3, max: 7)")
)

Creating the Assistant

gallery_assistant <- claude_new(
  sys_prompt = "You are an expert art gallery assistant. Help customers explore the gallery's collection, provide information about artworks and artists, calculate prices with available discounts, and assist with reservations. Be warm, knowledgeable, and helpful. When discussing art, share interesting insights about styles, techniques, and artists.",
  tools = list(
    search_gallery,
    get_artwork_details,
    calculate_price,
    place_hold
  ),
  temp = 0.7 # Balanced creativity and consistency
)

Example Conversations

# Customer browsing
gallery_assistant$chat(
  "Hi! I'm looking for something in the $3000-4000 range, preferably oil paintings. What do you have available?"
)

# Following up on a specific piece
gallery_assistant$chat(
  "The Sunset Harbor sounds beautiful. Can you tell me more about it and the artist?"
)

# Price inquiry with discount
gallery_assistant$chat(
  "I'm a gallery member. What would be the total price with my member discount?"
)

# Placing a hold
gallery_assistant$chat(
  "I'd like to reserve it. My name is Sarah Johnson and my email is sarah.johnson@email.com"
)

Combining Tools with Extended Thinking

For complex decisions, combine tools with extended thinking:

# Create an assistant with thinking enabled
analytical_assistant <- claude_new(
  sys_prompt = "You are an art investment advisor. Analyze the gallery's collection and provide thoughtful recommendations based on artistic merit, investment potential, and client preferences.",
  tools = list(search_gallery, get_artwork_details, calculate_price),
  think_effort = "high", # Enable deep reasoning
  interleaved = TRUE # Think between tool calls
)

# Complex analytical request
analytical_assistant$chat(
  "I'm a new collector with a $10,000 budget. I want to build a small collection
   of 2-3 pieces that will appreciate in value. Analyze your inventory and give
   me a reasoned recommendation with your investment thesis."
)

With interleaved = TRUE, Claude will:

  1. Search the inventory
  2. Think about what it found
  3. Get details on promising pieces
  4. Think about each artwork’s merits
  5. Calculate costs for different combinations
  6. Think through the investment thesis
  7. Provide a well-reasoned recommendation

Combining Tools with Structured Output

Extract structured data after tool interactions:

# Define the output schema
collection_schema <- ellmer::type_object(
  recommendations = ellmer::type_array(
    items = ellmer::type_object(
      artwork_id = ellmer::type_string("Artwork ID"),
      title = ellmer::type_string("Artwork title"),
      price = ellmer::type_number("Price in USD"),
      reasoning = ellmer::type_string("Why this piece is recommended")
    ),
    description = "List of recommended artworks"
  ),
  total_investment = ellmer::type_number("Total cost of recommendations"),
  investment_thesis = ellmer::type_string("Overall investment rationale")
)

# Get structured recommendations
result <- analytical_assistant$chat_structured(
  "Based on your analysis, provide your final recommendations in structured format.",
  type = collection_schema
)

# Result is now a structured R list
print(result$recommendations)
print(result$total_investment)

Error Handling Best Practices

Tools should handle errors gracefully:

robust_tool <- claude_tool(
  fn = function(artwork_id) {
    # Validate input
    if (!grepl("^ART\\d{3}$", artwork_id)) {
      return("Error: Invalid artwork ID format. Expected format: 'ART' followed by 3 digits (e.g., 'ART001').")
    }

    # Simulate potential failures
    result <- tryCatch(
      {
        # Database lookup
        dt <- .gallery_db$artworks[id == artwork_id]

        if (nrow(dt) == 0) {
          return(sprintf("No artwork found with ID '%s'. Use search_gallery to find valid artwork IDs.", artwork_id))
        }

        sprintf("Found: %s by %s", dt$title[1], dt$artist[1])
      },
      error = function(e) {
        # Log error for debugging
        message("Tool error: ", e$message)

        # Return user-friendly message
        "Sorry, there was an error accessing the artwork database. Please try again."
      }
    )

    result
  },
  name = "robust_lookup",
  desc = "Look up an artwork with comprehensive error handling",
  artwork_id = ellmer::type_string("Artwork ID in format 'ARTnnn'")
)

Key Error Handling Principles

  1. Validate inputs before processing
  2. Use tryCatch() around operations that might fail
  3. Return informative messages that help Claude (and users) understand what went wrong
  4. Suggest alternatives when possible (“Use search_gallery to find valid IDs”)
  5. Never expose internal errors to the user; log them for debugging

Multi-Turn Conversations with Tools

Tools work seamlessly across conversation turns:

chat <- claude_new(tools = list(search_gallery, get_artwork_details, place_hold))

# Turn 1: Initial inquiry
chat$chat("Show me available paintings under $4000")

# Turn 2: Follow-up (Claude remembers context)
chat$chat("Tell me more about the first one")

# Turn 3: Action
chat$chat("Great, let's put that on hold for John Smith, john@example.com")

# Turn 4: Confirmation
chat$chat("Perfect. What else would complement that piece?")

Claude maintains conversation context, so tool results from earlier turns inform later responses.

Performance Considerations

Tool Count and Context

Each tool definition consumes tokens. With many tools:

  • Consider grouping related functionality into fewer, more versatile tools
  • Lazy-load specialized tools only when needed
  • Remove tools that become irrelevant mid-conversation
# Instead of many specialized tools:
# - get_painting_info, get_sculpture_info, get_photograph_info

# Create one versatile tool:
get_artwork_info <- claude_tool(
  fn = function(artwork_id, info_type = "full") {
    # Handle all artwork types and info requests
  },
  name = "get_artwork_info",
  desc = "Get information about any artwork. Handles paintings, sculptures, photographs, and other media.",
  artwork_id = ellmer::type_string("Artwork ID"),
  info_type = ellmer::type_enum(
    c("full", "basic", "provenance", "exhibition_history"),
    "Type of information to retrieve"
  )
)

Response Size

Keep tool responses concise but complete:

  • Return the essential information Claude needs
  • Avoid dumping entire database tables
  • Structure output for easy parsing

Security Considerations

Input Validation

Never trust tool inputs:

# UNSAFE: SQL injection vulnerability
unsafe_tool <- claude_tool(
  fn = function(query) {
    DBI::dbGetQuery(con, sprintf("SELECT * FROM art WHERE title = '%s'", query))
  },
  name = "search",
  desc = "Search artworks",
  query = ellmer::type_string("Search term")
)

# SAFE: Parameterized query
safe_tool <- claude_tool(
  fn = function(query) {
    DBI::dbGetQuery(con, "SELECT * FROM art WHERE title = ?", params = list(query))
  },
  name = "search",
  desc = "Search artworks",
  query = ellmer::type_string("Search term")
)

File Path Validation

# UNSAFE: Path traversal vulnerability
unsafe_file_tool <- claude_tool(
  fn = function(filename) {
    readLines(filename) # Could read /etc/passwd!
  },
  name = "read_file",
  desc = "Read a file",
  filename = ellmer::type_string("File to read")
)

# SAFE: Restrict to allowed directory
safe_file_tool <- claude_tool(
  fn = function(filename) {
    # Normalize and validate path
    safe_dir <- normalizePath("/app/data", mustWork = TRUE)
    full_path <- normalizePath(file.path(safe_dir, basename(filename)), mustWork = FALSE)

    if (!startsWith(full_path, safe_dir)) {
      return("Error: Invalid file path")
    }

    if (!file.exists(full_path)) {
      return("Error: File not found")
    }

    paste(readLines(full_path), collapse = "\n")
  },
  name = "read_file",
  desc = "Read a data file from the app's data directory",
  filename = ellmer::type_string("Filename (not path) to read")
)

Rate Limiting

Protect expensive operations:

.rate_limiter <- new.env()
.rate_limiter$calls <- list()

rate_limited_tool <- claude_tool(
  fn = function(query) {
    # Check rate limit
    now <- Sys.time()
    window_start <- now - 60 # 1 minute window

    .rate_limiter$calls <- .rate_limiter$calls[
      sapply(.rate_limiter$calls, `>`, window_start)
    ]

    if (length(.rate_limiter$calls) >= 10) {
      return("Rate limit exceeded. Please wait before making more requests.")
    }

    .rate_limiter$calls <- c(.rate_limiter$calls, list(now))

    # Perform actual operation
    expensive_api_call(query)
  },
  name = "api_search",
  desc = "Search external API (rate limited to 10 calls/minute)",
  query = ellmer::type_string("Search query")
)

Testing Tools

Test your tools thoroughly before deployment:

library(testthat)

test_that("search_gallery returns results for valid queries", {
  result <- search_gallery$fn(artist = "Maria Chen")
  expect_true(grepl("Maria Chen", result))
  expect_true(grepl("Found", result))
})

test_that("search_gallery handles no results gracefully", {
  result <- search_gallery$fn(artist = "Unknown Artist")
  expect_true(grepl("No artworks found", result))
})

test_that("calculate_price applies discounts correctly", {
  result <- calculate_price$fn("ART001", "MEMBER10")
  expect_true(grepl("Member Discount", result))
  expect_true(grepl("-$450", result)) # 10% of $4500
})

test_that("calculate_price rejects invalid discount codes", {
  result <- calculate_price$fn("ART001", "INVALID")
  expect_true(grepl("Invalid discount code", result))
})

Summary

Custom tools transform Claude from a conversational AI into a powerful agent that can interact with your systems. Key takeaways:

  1. Define clear contracts: Write descriptive names and detailed descriptions
  2. Use appropriate types: Match parameter types to your function’s expectations
  3. Handle errors gracefully: Return informative messages, not exceptions
  4. Combine with thinking: Use think_effort and interleaved for complex reasoning
  5. Secure your tools: Validate inputs, restrict file access, rate limit expensive operations
  6. Test thoroughly: Verify tool behavior before deploying to production

Further Reading