tests/testthat/test-answer-as-json.R

test_that("answer_as_json (text-based) works without schema", {
  skip_test_if_no_openai()

  response <- "Create a very short persona" |>
    answer_as_json(type = "text-based") |>
    send_prompt(
      llm_provider_openai(
        url = "https://api.openai.com/v1/responses"
      )$set_parameters(list(model = "gpt-4.1-mini"))
    )

  expect_true(is.list(response), info = "Response should be a list")
})

test_that("answer_as_json (text-based) works with json schema", {
  skip_test_if_no_openai()

  schema <- list(
    "$schema" = "http://json-schema.org/draft-04/schema#",
    title = "Persona",
    type = "object",
    properties = list(
      name = list(type = "string", description = "The persona's name"),
      age = list(
        type = "integer",
        minimum = 0,
        description = "The persona's age"
      ),
      gender = list(
        type = "string",
        enum = c("Male", "Female", "Non-binary", "Other"),
        description = "The persona's gender"
      ),
      hobbies = list(
        type = "array",
        items = list(type = "string"),
        description = "List of hobbies"
      ),
      pet = list(
        type = "object",
        description = "Information about the persona's pet",
        properties = list(
          name = list(type = "string", description = "The pet's name"),
          age = list(
            type = "integer",
            minimum = 0,
            description = "The pet's age"
          ),
          species = list(
            type = "string",
            enum = c("Dog", "Cat", "Fish", "Bird", "Other"),
            description = "The pet's species"
          )
        ),
        required = c("name", "age", "species")
      )
    ),
    required = c("name", "age", "gender", "hobbies", "pet"),
    additionalProperties = FALSE
  )

  r_json_schema_to_example(schema)

  response <- "Create a persona" |>
    answer_as_json(schema, schema_strict = TRUE, type = "text-based") |>
    send_prompt(
      llm_provider_openai(
        url = "https://api.openai.com/v1/responses"
      )$set_parameters(list(model = "gpt-4.1-mini"))
    )

  expect_true(is.list(response), info = "Response should be a list")

  # Validate top-level properties
  expect_true(
    all(
      c(
        "name",
        "age",
        "gender",
        "hobbies",
        "pet"
      ) %in%
        names(response)
    )
  )

  # Validate `name`
  expect_true(is.character(response$name), info = "`name` should be a string")
  expect_true(nchar(response$name) > 0, info = "`name` should not be empty")

  # Validate `age`
  expect_true(is.numeric(response$age), info = "`age` should be numeric")
  expect_true(response$age >= 0, info = "`age` should be non-negative")

  # Validate `ge  nder`
  expect_true(
    is.character(response$gender),
    info = "`gender` should be a string"
  )
  expect_true(
    response$gender %in% c("Male", "Female", "Non-binary", "Other"),
    info = "`gender` should be one of the allowed values"
  )

  # Validate `hobbies`
  expect_true(
    is.list(response$hobbies) || is.character(response$hobbies),
    info = "`hobbies` should be a list or a character vector"
  )
  expect_true(
    all(sapply(response$hobbies, is.character)),
    info = "All items in `hobbies` should be strings"
  )

  # Validate `pet` object
  expect_true(is.list(response$pet), info = "`pet` should be a list")
  expect_true(
    is.character(response$pet$name),
    info = "`pet$name` should be a string"
  )
  expect_true(
    nchar(response$pet$name) > 0,
    info = "`pet$name` should not be empty"
  )
  expect_true(
    is.numeric(response$pet$age),
    info = "`pet$age` should be numeric"
  )
  expect_true(response$pet$age >= 0, info = "`pet$age` should be non-negative")
  expect_true(
    is.character(response$pet$species),
    info = "`pet$species` should be a string"
  )
  expect_true(
    response$pet$species %in% c("Dog", "Cat", "Fish", "Bird", "Other"),
    info = "`pet$species` should be one of the allowed values"
  )
})

test_that("answer_as_json (openai via auto) works", {
  skip_test_if_no_openai()

  schema <- list(
    "$schema" = "http://json-schema.org/draft-04/schema#",
    title = "Persona",
    type = "object",
    properties = list(
      name = list(type = "string", description = "The persona's name"),
      age = list(type = "integer", description = "The persona's age"),
      gender = list(
        type = "string",
        enum = c("Male", "Female", "Non-binary", "Other"),
        description = "The persona's gender"
      ),
      hobbies = list(
        type = "array",
        items = list(type = "string"),
        description = "List of hobbies"
      )
    ),
    required = c("name", "age", "gender", "hobbies"),
    additionalProperties = FALSE
  )

  expect_no_error(
    "Create a persona" |>
      answer_as_json(schema, type = "auto") |>
      send_prompt(llm_provider_openai())
  )

  expect_no_error(
    "Create a very short persona" |>
      answer_as_json(type = "auto") |>
      send_prompt(llm_provider_openai())
  )
})

test_that("answer_as_json (ollama via auto) works", {
  skip_test_if_no_ollama()

  schema <- list(
    "$schema" = "http://json-schema.org/draft-04/schema#",
    title = "Persona",
    type = "object",
    properties = list(
      name = list(type = "string", description = "The persona's name"),
      age = list(type = "integer", description = "The persona's age"),
      gender = list(
        type = "string",
        enum = c("Male", "Female", "Non-binary", "Other"),
        description = "The persona's gender"
      ),
      hobbies = list(
        type = "array",
        items = list(type = "string"),
        description = "List of hobbies"
      )
    ),
    required = c("name", "age", "gender", "hobbies"),
    additionalProperties = FALSE
  )

  expect_no_error(
    "Create a persona" |>
      answer_as_json(schema, type = "auto") |>
      send_prompt(llm_provider_ollama())
  )

  expect_no_error(
    "Create a very short persona" |>
      answer_as_json(type = "auto") |>
      send_prompt(llm_provider_ollama())
  )
})

test_that("answer_as_json (compatability with ellmer) works", {
  skip_test_if_no_openai()
  skip_if_not_installed("ellmer")

  # Persona validation function
  is_valid_persona <- function(persona) {
    if (!is.list(persona)) return(FALSE)

    required_fields <- c("name", "age", "hobbies")
    if (!all(required_fields %in% names(persona))) return(FALSE)

    if (!is.character(persona$name) || length(persona$name) != 1) return(FALSE)
    if (!is.numeric(persona$age) || length(persona$age) != 1) return(FALSE)
    if (
      !is.vector(persona$hobbies) || !all(sapply(persona$hobbies, is.character))
    )
      return(FALSE)

    TRUE
  }

  # Ellmer LLM provider
  ellmer_openai <- llm_provider_ellmer(ellmer::chat_openai(
    model = "gpt-4.1-mini"
  ))

  # Ellmer schema
  ellmer_schema <- ellmer::type_object(
    name = ellmer::type_string(),
    age = ellmer::type_integer(),
    hobbies = ellmer::type_array(ellmer::type_string())
  )

  # Ellmer LLM provider with Ellmer schema
  result_ellmer_x_ellmer <- "Create a persona" |>
    answer_as_json(ellmer_schema) |>
    send_prompt(ellmer_openai)
  expect_true(is_valid_persona(result_ellmer_x_ellmer))

  # Regular LLM provider with Ellmer schema
  result_tidyrpompt_x_ellmer <- "Create a persona" |>
    answer_as_json(ellmer_schema) |>
    send_prompt(
      llm_provider_openai(
        url = "https://api.openai.com/v1/responses"
      )$set_parameters(list(model = "gpt-4.1-mini"))
    )
  expect_true(is_valid_persona(result_tidyrpompt_x_ellmer))

  # Regular schema
  schema <- list(
    "$schema" = "http://json-schema.org/draft-04/schema#",
    title = "Persona",
    type = "object",
    properties = list(
      name = list(type = "string", description = "The persona's name"),
      age = list(type = "integer", description = "The persona's age"),
      hobbies = list(
        type = "array",
        items = list(type = "string"),
        description = "List of hobbies"
      )
    ),
    required = c("name", "age", "hobbies"),
    additionalProperties = FALSE
  )

  # Ellmer LLM provider with regular schema
  result_ellmer_x_regular <- "Create a persona" |>
    answer_as_json(schema) |>
    send_prompt(ellmer_openai)
  expect_true(is_valid_persona(result_ellmer_x_regular))
})

test_that("answer_as_json + llm_provider_ellmer: scalar types", {
  skip_test_if_no_openai()
  skip_if_not_installed("ellmer")

  ellmer_openai <- llm_provider_ellmer(ellmer::chat_openai(
    model = "gpt-4.1-mini"
  ))

  # type_string()
  res_str <- "Return the string 'hello' only." |>
    answer_as_json(ellmer::type_string()) |>
    send_prompt(ellmer_openai)
  expect_true(is.character(res_str) && length(res_str) == 1)

  # type_integer()
  res_int <- "Return the integer 7 only." |>
    answer_as_json(ellmer::type_integer()) |>
    send_prompt(ellmer_openai)
  expect_true(is.numeric(res_int) && length(res_int) == 1)
  expect_equal(as.integer(res_int), 7L)

  # type_number()
  res_num <- "Return the number 3.14 only." |>
    answer_as_json(ellmer::type_number()) |>
    send_prompt(ellmer_openai)
  expect_true(is.numeric(res_num) && length(res_num) == 1)
  expect_gt(res_num, 3)
  expect_lt(res_num, 3.2)

  # type_boolean()
  res_bool <- "Return the boolean true only." |>
    answer_as_json(ellmer::type_boolean()) |>
    send_prompt(ellmer_openai)
  expect_true(is.logical(res_bool) && length(res_bool) == 1)

  # type_enum()
  allowed <- c("Technology", "Sports", "Politics")
  res_enum <- "Categorize: 'AI chips surge in demand'. Choose one of Technology, Sports, Politics; return only the category." |>
    answer_as_json(ellmer::type_enum(allowed)) |>
    send_prompt(ellmer_openai)
  enum_val <- if (is.factor(res_enum)) as.character(res_enum) else res_enum
  expect_true(is.character(enum_val) && enum_val %in% allowed)
})

test_that("answer_as_json + llm_provider_ellmer: arrays of scalars", {
  skip_test_if_no_openai()
  skip_if_not_installed("ellmer")

  ellmer_openai <- llm_provider_ellmer(ellmer::chat_openai(
    model = "gpt-4.1-mini"
  ))

  # vector of integers
  res_int_vec <- "Return exactly: 18, 21, 30." |>
    answer_as_json(ellmer::type_array(ellmer::type_integer())) |>
    send_prompt(ellmer_openai)
  expect_true(is.numeric(res_int_vec))
  expect_length(res_int_vec, 3)
  expect_equal(as.integer(res_int_vec), c(18L, 21L, 30L))

  # vector of numbers
  res_num_vec <- "Return exactly: 1.5, 2.0, 2.5." |>
    answer_as_json(ellmer::type_array(ellmer::type_number())) |>
    send_prompt(ellmer_openai)
  expect_true(is.numeric(res_num_vec))
  expect_equal(length(res_num_vec), 3)
  expect_true(all(res_num_vec > 0))

  # vector of enums
  allowed <- c("red", "green", "blue")
  res_enum_vec <- "Return these colours in order: red, green, blue." |>
    answer_as_json(ellmer::type_array(ellmer::type_enum(allowed))) |>
    send_prompt(ellmer_openai)
  expect_true(is.character(res_enum_vec) || is.factor(res_enum_vec))
  if (is.factor(res_enum_vec)) res_enum_vec <- as.character(res_enum_vec)
  expect_equal(res_enum_vec, allowed)
})

test_that("answer_as_json + llm_provider_ellmer: arrays of objects -> data.frame", {
  skip_test_if_no_openai()
  skip_if_not_installed("ellmer")

  ellmer_openai <- llm_provider_ellmer(ellmer::chat_openai(
    model = "gpt-4.1-mini"
  ))

  prompt <- r"(
    * John Smith. Age: 30. Height: 180 cm. Weight: 80 kg.
    * Jane Doe. Age: 25. Height: 165 cm. Weight: 50 kg.
    * Jose Rodriguez. Age: 40. Height: 190 cm. Weight: 90 kg.
  )"

  type_person <- ellmer::type_object(
    name = ellmer::type_string(),
    age = ellmer::type_integer(),
    height = ellmer::type_number("in cm"),
    weight = ellmer::type_number("in kg")
  )
  type_people <- ellmer::type_array(type_person)

  df <- prompt |>
    answer_as_json(type_people) |>
    send_prompt(ellmer_openai)

  expect_s3_class(df, "data.frame")
  expect_true(all(c("name", "age", "height", "weight") %in% names(df)))
  expect_equal(nrow(df), 3)
  expect_true(is.character(df$name))
  expect_true(
    is.numeric(df$age) && is.numeric(df$height) && is.numeric(df$weight)
  )
})

test_that("answer_as_json + llm_provider_ellmer: optional fields (required = FALSE)", {
  skip_test_if_no_openai()
  skip_if_not_installed("ellmer")

  ellmer_openai <- llm_provider_ellmer(ellmer::chat_openai(
    model = "gpt-4.1-mini"
  ))

  type_person_opt <- ellmer::type_object(
    name = ellmer::type_string(required = FALSE),
    age = ellmer::type_integer(required = FALSE)
  )

  # Only age present
  res_age_only <- "I'm 33 years old." |>
    answer_as_json(type_person_opt) |>
    send_prompt(ellmer_openai)
  expect_true(is.list(res_age_only))
  expect_true(all(names(res_age_only) %in% c("name", "age")))
  if ("age" %in% names(res_age_only)) {
    expect_true(is.numeric(res_age_only$age) && length(res_age_only$age) == 1)
  }

  # Only name present
  res_name_only <- "My name is Taylor." |>
    answer_as_json(type_person_opt) |>
    send_prompt(ellmer_openai)
  expect_true(is.list(res_name_only))
  expect_true(all(names(res_name_only) %in% c("name", "age")))
  if ("name" %in% names(res_name_only)) {
    expect_true(
      is.character(res_name_only$name) && nchar(res_name_only$name) > 0
    )
  }
})

test_that("answer_as_json + llm_provider_ellmer: arrays of enums and objects combined", {
  skip_test_if_no_openai()
  skip_if_not_installed("ellmer")

  ellmer_openai <- llm_provider_ellmer(ellmer::chat_openai(
    model = "gpt-4.1-mini"
  ))

  # Object with an enum and an array of enums
  type_pref <- ellmer::type_object(
    primary = ellmer::type_enum(c("red", "green", "blue")),
    fallback = ellmer::type_array(ellmer::type_enum(c("red", "green", "blue")))
  )

  res_pref <- "Primary colour is green; fallbacks are red then blue." |>
    answer_as_json(type_pref) |>
    send_prompt(ellmer_openai)

  expect_true(is.list(res_pref))
  primary <- if (is.factor(res_pref$primary))
    as.character(res_pref$primary) else res_pref$primary
  expect_true(primary %in% c("red", "green", "blue"))

  fallbacks <- res_pref$fallback
  if (is.factor(fallbacks)) fallbacks <- as.character(fallbacks)
  expect_true(is.character(fallbacks))
  expect_true(all(fallbacks %in% c("red", "green", "blue")))
})

Try the tidyprompt package in your browser

Any scripts or data that you put into this service are public.

tidyprompt documentation built on Aug. 25, 2025, 5:11 p.m.