blob: c91447bdd8bcf1cbca7d7bfd22f9e4a90f4026d0 [file] [log] [blame]
Marc Kupietz323a4d32026-09-03 12:04:35 +02001# Models the documentation is prompted with. Cheap, fast models are used on
2# purpose: the tasks are simple, and if such a model cannot follow the Readme,
3# that says something about the Readme, which is what is being tested here.
4# Override with a comma separated list in RKORAP_LLM_MODELS, e.g. to add
5# OpenAI's cheapest models, "gpt-5-nano" or "gpt-5.6-luna".
6defaultLlmModels <- c(
7 "gemini-3.5-flash-lite",
8 "claude-sonnet-5",
9 "hf:zai-org/GLM-5.3-Flash" # GLM-5.3-Flash, via the Synthetic API
10)
Marc Kupietza28a99a2025-07-06 14:52:47 +020011
Marc Kupietz323a4d32026-09-03 12:04:35 +020012llmModels <- function() {
Marc Kupietz6957ac92026-09-03 12:18:27 +020013 configured <- Sys.getenv("RKORAP_LLM_MODELS", unset = NA_character_)
14 if (is.na(configured)) {
15 return(defaultLlmModels)
Marc Kupietz323a4d32026-09-03 12:04:35 +020016 }
Marc Kupietz6957ac92026-09-03 12:18:27 +020017 # set but empty means no models at all, which is how these tests are kept out
18 # of the pipeline job that runs everything else
19 models <- trimws(strsplit(configured, ",", fixed = TRUE)[[1]])
20 models[nzchar(models)]
Marc Kupietz323a4d32026-09-03 12:04:35 +020021}
22
23# Provider and API key environment variable belonging to a model id
24llmProvider <- function(model) {
25 if (grepl("^gpt-", model, ignore.case = TRUE)) {
26 list(name = "openai", keyVar = "OPENAI_API_KEY")
27 } else if (grepl("^claude-", model, ignore.case = TRUE)) {
28 list(name = "claude", keyVar = "ANTHROPIC_API_KEY")
29 } else if (grepl("^gemini-", model, ignore.case = TRUE)) {
30 list(name = "gemini", keyVar = "GOOGLE_API_KEY")
31 } else if (grepl("^hf:", model, ignore.case = TRUE)) {
32 # OpenAI compatible endpoint, but tidyllm's openai provider does not allow
33 # for a custom base url, so these are queried directly (see below)
34 list(name = "synthetic", keyVar = "SYNTHETIC_API_KEY")
35 } else {
36 stop(paste(
37 "Unsupported model:", model,
38 "- supported prefixes: gpt-, claude-, gemini-, hf: (Synthetic)"
39 ))
40 }
41}
42
43# Helper function to skip if the API key of the given model is not available
44skip_if_no_api_key <- function(model) {
45 keyVar <- llmProvider(model)$keyVar
Marc Kupietz2deadd82025-07-09 08:53:33 +020046 skip_if_not(
Marc Kupietz323a4d32026-09-03 12:04:35 +020047 nzchar(Sys.getenv(keyVar)),
48 paste0("No API key for ", model, " found (need ", keyVar, ")")
Marc Kupietz2deadd82025-07-09 08:53:33 +020049 )
Marc Kupietzc9cb6772025-07-06 15:55:00 +020050}
51
Marc Kupietz06143702025-07-05 17:49:31 +020052# Helper function to find README.md file in current or parent directories
53find_readme_path <- function() {
54 readme_paths <- c("Readme.md", "../Readme.md", "../../Readme.md")
55 for (path in readme_paths) {
56 if (file.exists(path)) {
57 return(path)
58 }
59 }
60 return(NULL)
61}
62
63# Helper function to read README content
64read_readme_content <- function() {
65 readme_path <- find_readme_path()
66 if (is.null(readme_path)) {
67 return(NULL)
68 }
69 readme_content <- readLines(readme_path)
Marc Kupietz2deadd82025-07-09 08:53:33 +020070
71 # Find the line with "## Installation" and truncate before it
72 installation_line <- grep("^## Installation", readme_content, ignore.case = TRUE)
73 if (length(installation_line) > 0) {
74 readme_content <- readme_content[1:(installation_line[1] - 1)]
75 }
76
Marc Kupietz06143702025-07-05 17:49:31 +020077 paste(readme_content, collapse = "\n")
78}
79
Marc Kupietz323a4d32026-09-03 12:04:35 +020080# Helper function to call an OpenAI compatible endpoint that tidyllm cannot be
81# pointed at, because its openai provider takes no custom base url
82call_openai_compatible_api <- function(prompt, model, temperature, baseUrl, keyVar) {
83 response <- httr2::request(paste0(baseUrl, "/chat/completions")) |>
84 httr2::req_auth_bearer_token(Sys.getenv(keyVar)) |>
85 httr2::req_body_json(list(
86 model = model,
87 temperature = temperature,
88 messages = list(list(role = "user", content = prompt))
89 )) |>
90 httr2::req_retry(max_tries = 3) |>
91 httr2::req_timeout(120) |>
92 httr2::req_perform()
93
94 httr2::resp_body_json(response)$choices[[1]]$message$content
95}
96
Marc Kupietz345211a2025-07-06 12:52:24 +020097# Helper function to call LLM API using tidyllm
Marc Kupietz323a4d32026-09-03 12:04:35 +020098call_llm_api <- function(prompt, model, max_tokens = 500, temperature = 0.1) {
Marc Kupietz2deadd82025-07-09 08:53:33 +020099 cat("Calling LLM API with model:", model, "\n")
100 # Only print prompt up to the beginning of README content
101 readme_start <- regexpr("README Documentation:", prompt, fixed = TRUE)
102 if (readme_start > 0) {
103 prompt_preview <- substr(prompt, 1, readme_start - 1)
104 cat("Prompt (up to README):\n", prompt_preview, "\n")
105 } else {
106 cat("Prompt:\n", prompt, "\n")
107 }
108 tryCatch(
109 {
Marc Kupietz323a4d32026-09-03 12:04:35 +0200110 provider <- llmProvider(model)
Marc Kupietz06143702025-07-05 17:49:31 +0200111
Marc Kupietz323a4d32026-09-03 12:04:35 +0200112 if (provider$name == "synthetic") {
113 call_openai_compatible_api(
114 prompt,
115 model = model,
116 temperature = temperature,
117 baseUrl = "https://api.synthetic.new/openai/v1",
118 keyVar = provider$keyVar
Marc Kupietz2deadd82025-07-09 08:53:33 +0200119 )
Marc Kupietz323a4d32026-09-03 12:04:35 +0200120 } else {
121 # Use tidyllm unified API
122 result <- tidyllm::llm_message(prompt) |>
123 tidyllm::chat(
124 .provider = switch(provider$name,
125 openai = tidyllm::openai(),
126 claude = tidyllm::claude(),
127 gemini = tidyllm::gemini()
128 ),
129 .model = model,
130 .temperature = temperature,
131 .max_tries = 3
132 )
Marc Kupietz345211a2025-07-06 12:52:24 +0200133
Marc Kupietz323a4d32026-09-03 12:04:35 +0200134 # Extract the reply text
135 tidyllm::get_reply(result)
136 }
Marc Kupietz2deadd82025-07-09 08:53:33 +0200137 },
138 error = function(e) {
Marc Kupietz323a4d32026-09-03 12:04:35 +0200139 message <- as.character(e)
140 # Conditions of the account rather than of the documentation: these must
141 # not turn a documentation test red
142 if (grepl("429", message)) {
Marc Kupietz2deadd82025-07-09 08:53:33 +0200143 skip("LLM API rate limit exceeded - please try again later or check your API key/credits")
Marc Kupietz323a4d32026-09-03 12:04:35 +0200144 } else if (grepl("401|403", message)) {
145 skip(paste0(
146 "LLM API authentication failed - please check ",
147 llmProvider(model)$keyVar
148 ))
149 } else if (grepl("402|credit balance|billing|quota|insufficient", message, ignore.case = TRUE)) {
150 skip(paste0("No credits available for ", model, ": ", message))
Marc Kupietz2deadd82025-07-09 08:53:33 +0200151 } else {
Marc Kupietz323a4d32026-09-03 12:04:35 +0200152 stop(paste("LLM API error:", message))
Marc Kupietz2deadd82025-07-09 08:53:33 +0200153 }
Marc Kupietz06143702025-07-05 17:49:31 +0200154 }
Marc Kupietz2deadd82025-07-09 08:53:33 +0200155 )
Marc Kupietz06143702025-07-05 17:49:31 +0200156}
157
158# Helper function to create README-guided prompt
Marc Kupietzffe28aa2026-09-05 17:50:45 +0200159#
160# Shaped the way a user would reasonably prompt, since what is tested here is
161# also what we recommend: the documentation first, the task once and at the end,
162# and the expectations stated rather than left to a persona ("you are an expert
163# R programmer" moves the wording of an answer, not its correctness).
164create_readme_prompt <- function(task_description) {
Marc Kupietz06143702025-07-05 17:49:31 +0200165 readme_text <- read_readme_content()
166 if (is.null(readme_text)) {
167 stop("README.md not found")
168 }
169
170 paste0(
Marc Kupietzffe28aa2026-09-05 17:50:45 +0200171 "The following is the README of the R package RKorAPClient.\n\n",
Marc Kupietz06143702025-07-05 17:49:31 +0200172 readme_text,
Marc Kupietzffe28aa2026-09-05 17:50:45 +0200173 "\n\nTask, based on that documentation: ", task_description,
174 "\n\nWrite clear, idiomatic tidyverse code, in the style of the README's",
175 " own examples. Answer with a single R code block and nothing else."
Marc Kupietz06143702025-07-05 17:49:31 +0200176 )
177}
178
179# Helper function to extract R code from markdown code blocks
180extract_r_code <- function(response_text) {
Marc Kupietz08cc2e52026-09-05 16:49:25 +0200181 # Asked for code alone, a model may still explain itself around it, or offer a
182 # second way of doing the same in a block of its own. Only the first block is
183 # what was asked for; stripping the fences and keeping everything else puts
184 # the prose in between into the code, where it does not parse.
185 block <- stringr::str_match(response_text, "(?s)```[^\\n]*\\n(.*?)```")[1, 2]
186 trimws(if (is.na(block)) response_text else block)
Marc Kupietz06143702025-07-05 17:49:31 +0200187}
188
Marc Kupietz08cc2e52026-09-05 16:49:25 +0200189test_that("the first code block is what is taken from a reply", {
190 fenced <- function(...) paste(c(...), collapse = "\n")
191
192 expect_equal(
193 extract_r_code(fenced("```r", "corpusStats(kco)", "```")),
194 "corpusStats(kco)"
195 )
196 # prose around the block does not belong to the code
197 expect_equal(
198 extract_r_code(fenced("Here you are:", "```R", "corpusStats(kco)", "```", "Hope that helps!")),
199 "corpusStats(kco)"
200 )
201 # a second block, offered as an alternative, would not parse together with the
202 # sentence introducing it
203 expect_equal(
204 extract_r_code(fenced(
205 "```r", "corpusStats(kco)", "```",
206 "Or, as a data frame:",
207 "```r", "corpusStats(kco, as.df = TRUE)", "```"
208 )),
209 "corpusStats(kco)"
210 )
211 # a reply that took "only the R code" literally has no fences to look for
212 expect_equal(extract_r_code("corpusStats(kco)"), "corpusStats(kco)")
213})
214
Marc Kupietze759b342025-07-05 19:48:20 +0200215# Helper function to test code syntax
216test_code_syntax <- function(code) {
Marc Kupietz2deadd82025-07-09 08:53:33 +0200217 tryCatch(
218 {
219 parse(text = code)
220 TRUE
221 },
222 error = function(e) {
223 cat("Syntax error:", as.character(e), "\n")
224 FALSE
225 }
226 )
Marc Kupietze759b342025-07-05 19:48:20 +0200227}
228
229# Helper function to run code if RUN_LLM_CODE is set
230run_code_if_enabled <- function(code, test_name) {
231 if (nzchar(Sys.getenv("RUN_LLM_CODE")) && Sys.getenv("RUN_LLM_CODE") == "true") {
232 cat("Running generated code for", test_name, "...\n")
Marc Kupietz2deadd82025-07-09 08:53:33 +0200233 tryCatch(
234 {
235 result <- eval(parse(text = code))
236 cat("Code executed successfully. Result type:", class(result), "\n")
237 if (is.data.frame(result)) {
238 cat("Result dimensions:", nrow(result), "rows,", ncol(result), "columns\n")
239 if (nrow(result) > 0) {
240 cat("First few rows:\n")
241 print(head(result, 3))
242 }
243 } else {
244 cat("Result preview:\n")
245 print(result)
Marc Kupietze759b342025-07-05 19:48:20 +0200246 }
Marc Kupietz2deadd82025-07-09 08:53:33 +0200247 return(TRUE)
248 },
249 error = function(e) {
250 cat("Runtime error:", as.character(e), "\n")
251 return(FALSE)
Marc Kupietze759b342025-07-05 19:48:20 +0200252 }
Marc Kupietz2deadd82025-07-09 08:53:33 +0200253 )
Marc Kupietze759b342025-07-05 19:48:20 +0200254 } else {
255 cat("Skipping code execution (set RUN_LLM_CODE=true to enable)\n")
256 return(NA)
257 }
258}
259
Marc Kupietz323a4d32026-09-03 12:04:35 +0200260for (model in llmModels()) {
261 test_that(paste(model, "can solve frequency query task with README guidance"), {
262 # Skip if offline
263 skip_if_offline()
Marc Kupietz2deadd82025-07-09 08:53:33 +0200264
Marc Kupietz323a4d32026-09-03 12:04:35 +0200265 # Skip if no API keys are set
266 skip_if_no_api_key(model)
Marc Kupietz2deadd82025-07-09 08:53:33 +0200267
Marc Kupietz323a4d32026-09-03 12:04:35 +0200268 # tidyllm is only suggested, so the tests must not fail without it
269 if (llmProvider(model)$name != "synthetic") skip_if_not_installed("tidyllm")
Marc Kupietz06143702025-07-05 17:49:31 +0200270
Marc Kupietz323a4d32026-09-03 12:04:35 +0200271 # Check for README file
272 skip_if_not(!is.null(find_readme_path()), "Readme.md not found in current or parent directories")
Marc Kupietz06143702025-07-05 17:49:31 +0200273
Marc Kupietz323a4d32026-09-03 12:04:35 +0200274 # Create the prompt with README context and task
275 prompt <- create_readme_prompt(
Marc Kupietzffe28aa2026-09-05 17:50:45 +0200276 "write R code to perform a frequency query for the word 'Demokratie' across the past three years. The code should use the RKorAPClient package and return a data frame."
Marc Kupietz323a4d32026-09-03 12:04:35 +0200277 )
Marc Kupietz06143702025-07-05 17:49:31 +0200278
Marc Kupietz323a4d32026-09-03 12:04:35 +0200279 # Call LLM API
280 generated_response <- call_llm_api(prompt, model, max_tokens = 500)
281 generated_code <- extract_r_code(generated_response)
Marc Kupietzb8839d32025-07-06 14:42:30 +0200282
Marc Kupietz323a4d32026-09-03 12:04:35 +0200283 # Basic checks on the generated code
284 expect_true(grepl("KorAPConnection", generated_code), "Generated code should include KorAPConnection")
285 expect_true(grepl("frequencyQuery", generated_code), "Generated code should include frequencyQuery")
286 expect_true(grepl("Demokratie", generated_code), "Generated code should include the search term 'Demokratie'")
287 last_year <- as.numeric(format(Sys.Date(), "%Y")) - 1
Marc Kupietz06143702025-07-05 17:49:31 +0200288
Marc Kupietz323a4d32026-09-03 12:04:35 +0200289 expect_true(grepl("Date in", generated_code), "Generated code should vc restriction on years")
Marc Kupietz06143702025-07-05 17:49:31 +0200290
Marc Kupietz323a4d32026-09-03 12:04:35 +0200291 # Check that the generated code contains essential RKorAPClient patterns
292 # expect_true(grepl("\\|>", generated_code) || grepl("%>%", generated_code), "Generated code should use pipe operators")
Marc Kupietz06143702025-07-05 17:49:31 +0200293
Marc Kupietz323a4d32026-09-03 12:04:35 +0200294 # Test code syntax
295 syntax_valid <- test_code_syntax(generated_code)
296 expect_true(syntax_valid, "Generated code should be syntactically valid R code")
Marc Kupietze759b342025-07-05 19:48:20 +0200297
Marc Kupietz323a4d32026-09-03 12:04:35 +0200298 # Print the generated code for manual inspection
299 cat("Generated code:\n", generated_code, "\n")
300
301 # Run the code if RUN_LLM_CODE is set
302 execution_result <- run_code_if_enabled(generated_code, "frequency query")
303 if (!is.na(execution_result)) {
304 expect_true(execution_result, "Generated code should execute without runtime errors")
305 }
306 })
Marc Kupietz06143702025-07-05 17:49:31 +0200307
Marc Kupietz345211a2025-07-06 12:52:24 +0200308
Marc Kupietz323a4d32026-09-03 12:04:35 +0200309 test_that(paste(model, "can solve collocation analysis task with README guidance"), {
310 # Skip if offline
311 skip_if_offline()
Marc Kupietz2deadd82025-07-09 08:53:33 +0200312
Marc Kupietz323a4d32026-09-03 12:04:35 +0200313 # Skip if no API keys are set
314 skip_if_no_api_key(model)
Marc Kupietz2deadd82025-07-09 08:53:33 +0200315
Marc Kupietz323a4d32026-09-03 12:04:35 +0200316 # tidyllm is only suggested, so the tests must not fail without it
317 if (llmProvider(model)$name != "synthetic") skip_if_not_installed("tidyllm")
Marc Kupietz06143702025-07-05 17:49:31 +0200318
Marc Kupietz323a4d32026-09-03 12:04:35 +0200319 # Check for README file
320 skip_if_not(!is.null(find_readme_path()), "Readme.md not found in current or parent directories")
Marc Kupietz06143702025-07-05 17:49:31 +0200321
Marc Kupietz323a4d32026-09-03 12:04:35 +0200322 # Create the prompt for collocation analysis
323 prompt <- create_readme_prompt(
324 paste("Write R code to perform a collocation analysis for the lemma 'leverage' based on the current English Wikipedia Corpus using default parameters", "and show the three highest collocates according to their log dice score.
Marc Kupietzffe28aa2026-09-05 17:50:45 +0200325 ")
Marc Kupietz323a4d32026-09-03 12:04:35 +0200326 )
Marc Kupietz06143702025-07-05 17:49:31 +0200327
Marc Kupietz323a4d32026-09-03 12:04:35 +0200328 # Call LLM API
329 generated_response <- call_llm_api(prompt, model, max_tokens = 500)
330 generated_code <- extract_r_code(generated_response)
Marc Kupietz06143702025-07-05 17:49:31 +0200331
Marc Kupietz323a4d32026-09-03 12:04:35 +0200332 # Basic checks on the generated code
333 expect_true(grepl("KorAPConnection", generated_code), "Generated code should include KorAPConnection")
334 expect_true(grepl("collocationAnalysis", generated_code), "Generated code should include collocationAnalysis")
Marc Kupietz3321dae2026-09-05 15:30:52 +0200335 # both ways of asking for the lemma are correct: the annotation layer in the
336 # query, as the Readme shows it, or collocationAnalysis' lemmatizeNodeQuery,
337 # which builds the same query from a plain word
338 expect_true(grepl("leverage", generated_code), "Generated code should include the node 'leverage'")
339 expect_true(
340 grepl("tt/l=leverage", generated_code) ||
341 grepl("lemmatizeNodeQuery\\s*=\\s*T", generated_code),
342 "Generated code should search for the lemma, via tt/l= or lemmatizeNodeQuery = TRUE"
343 )
Marc Kupietz323a4d32026-09-03 12:04:35 +0200344 # expect_true(grepl("auth", generated_code), "Generated code should include auth() for collocation analysis")
345 expect_true(grepl("instance/english", generated_code, fixed = TRUE), "Generated code should include the specified KorAP URL")
Marc Kupietze759b342025-07-05 19:48:20 +0200346
Marc Kupietz323a4d32026-09-03 12:04:35 +0200347 # Test code syntax
348 syntax_valid <- test_code_syntax(generated_code)
349 expect_true(syntax_valid, "Generated code should be syntactically valid R code")
Marc Kupietze759b342025-07-05 19:48:20 +0200350
Marc Kupietz323a4d32026-09-03 12:04:35 +0200351 # Print the generated code for manual inspection
352 cat("Generated collocation analysis code:\n", generated_code, "\n")
Marc Kupietz06143702025-07-05 17:49:31 +0200353
Marc Kupietz323a4d32026-09-03 12:04:35 +0200354 # Run the code if RUN_LLM_CODE is set
355 execution_result <- run_code_if_enabled(generated_code, "collocation analysis")
356 if (!is.na(execution_result)) {
357 expect_true(execution_result, "Generated code should execute without runtime errors")
358 }
359 })
Marc Kupietz2deadd82025-07-09 08:53:33 +0200360
Marc Kupietz323a4d32026-09-03 12:04:35 +0200361 test_that(paste(model, "can solve corpus query task with README guidance"), {
362 # Skip if offline
363 skip_if_offline()
Marc Kupietz2deadd82025-07-09 08:53:33 +0200364
Marc Kupietz323a4d32026-09-03 12:04:35 +0200365 # Skip if no API keys are set
366 skip_if_no_api_key(model)
Marc Kupietz06143702025-07-05 17:49:31 +0200367
Marc Kupietz323a4d32026-09-03 12:04:35 +0200368 # tidyllm is only suggested, so the tests must not fail without it
369 if (llmProvider(model)$name != "synthetic") skip_if_not_installed("tidyllm")
Marc Kupietz06143702025-07-05 17:49:31 +0200370
Marc Kupietz323a4d32026-09-03 12:04:35 +0200371 # Check for README file
372 skip_if_not(!is.null(find_readme_path()), "Readme.md not found in current or parent directories")
Marc Kupietz06143702025-07-05 17:49:31 +0200373
Marc Kupietz323a4d32026-09-03 12:04:35 +0200374 # Create the prompt for corpus query
375 prompt <- create_readme_prompt(
Marc Kupietzffe28aa2026-09-05 17:50:45 +0200376 "write R code to perform a simple corpus query for 'Hello world' and fetch all results. The code should use the RKorAPClient package."
Marc Kupietz323a4d32026-09-03 12:04:35 +0200377 )
Marc Kupietz06143702025-07-05 17:49:31 +0200378
Marc Kupietz323a4d32026-09-03 12:04:35 +0200379 # Call LLM API
380 generated_response <- call_llm_api(prompt, model, max_tokens = 300)
381 generated_code <- extract_r_code(generated_response)
Marc Kupietz06143702025-07-05 17:49:31 +0200382
Marc Kupietz323a4d32026-09-03 12:04:35 +0200383 # Basic checks on the generated code
384 expect_true(grepl("KorAPConnection", generated_code), "Generated code should include KorAPConnection")
385 expect_true(grepl("corpusQuery", generated_code), "Generated code should include corpusQuery")
386 expect_true(grepl("Hello world", generated_code), "Generated code should include the search term 'Hello world'")
387 expect_true(grepl("fetchAll", generated_code), "Generated code should include fetchAll")
Marc Kupietze759b342025-07-05 19:48:20 +0200388
Marc Kupietz323a4d32026-09-03 12:04:35 +0200389 # Check that the generated code follows the README example pattern
390 expect_true(
391 grepl("\\|>", generated_code) || grepl("%>%", generated_code),
392 "Generated code should use pipe operators"
393 )
Marc Kupietze759b342025-07-05 19:48:20 +0200394
Marc Kupietz323a4d32026-09-03 12:04:35 +0200395 # Test code syntax
396 syntax_valid <- test_code_syntax(generated_code)
397 expect_true(syntax_valid, "Generated code should be syntactically valid R code")
398
399 # Print the generated code for manual inspection
400 cat("Generated corpus query code:\n", generated_code, "\n")
401
402 # Run the code if RUN_LLM_CODE is set
403 execution_result <- run_code_if_enabled(generated_code, "corpus query")
404 if (!is.na(execution_result)) {
405 expect_true(execution_result, "Generated code should execute without runtime errors")
406 }
407 })
Marc Kupietz81ddc342026-09-03 13:18:53 +0200408
409 test_that(paste(model, "can solve corpus size task with README guidance"), {
410 skip_if_offline()
411 skip_if_no_api_key(model)
412 if (llmProvider(model)$name != "synthetic") skip_if_not_installed("tidyllm")
413 skip_if_not(!is.null(find_readme_path()), "Readme.md not found in current or parent directories")
414
415 prompt <- create_readme_prompt(
Marc Kupietzffe28aa2026-09-05 17:50:45 +0200416 "write R code that reports how many tokens the virtual corpus of newspaper texts published since 2020 contains."
Marc Kupietz81ddc342026-09-03 13:18:53 +0200417 )
418
419 generated_code <- extract_r_code(call_llm_api(prompt, model, max_tokens = 300))
420
421 expect_true(grepl("KorAPConnection", generated_code), "Generated code should include KorAPConnection")
422 expect_true(grepl("corpusStats", generated_code), "Generated code should include corpusStats")
423 expect_true(grepl("vc", generated_code), "Generated code should restrict to a virtual corpus")
424 expect_true(test_code_syntax(generated_code), "Generated code should be syntactically valid R code")
425
426 cat("Generated corpus size code:\n", generated_code, "\n")
427 })
428
429 test_that(paste(model, "can solve text metadata task with README guidance"), {
430 skip_if_offline()
431 skip_if_no_api_key(model)
432 if (llmProvider(model)$name != "synthetic") skip_if_not_installed("tidyllm")
433 skip_if_not(!is.null(find_readme_path()), "Readme.md not found in current or parent directories")
434
435 prompt <- create_readme_prompt(
Marc Kupietzffe28aa2026-09-05 17:50:45 +0200436 "write R code that retrieves all metadata KorAP holds for the text with the sigle WPD17/L79/98721."
Marc Kupietz81ddc342026-09-03 13:18:53 +0200437 )
438
439 generated_code <- extract_r_code(call_llm_api(prompt, model, max_tokens = 300))
440
441 expect_true(grepl("KorAPConnection", generated_code), "Generated code should include KorAPConnection")
442 expect_true(grepl("textMetadata", generated_code), "Generated code should include textMetadata")
443 expect_true(grepl("WPD17/L79/98721", generated_code, fixed = TRUE), "Generated code should include the text sigle")
444 expect_true(test_code_syntax(generated_code), "Generated code should be syntactically valid R code")
445
446 cat("Generated text metadata code:\n", generated_code, "\n")
447 })
448
449 test_that(paste(model, "can solve association score task with README guidance"), {
450 skip_if_offline()
451 skip_if_no_api_key(model)
452 if (llmProvider(model)$name != "synthetic") skip_if_not_installed("tidyllm")
453 skip_if_not(!is.null(find_readme_path()), "Readme.md not found in current or parent directories")
454
455 prompt <- create_readme_prompt(
456 paste(
457 "write R code that computes association scores for the word 'Grund' together with each of the",
458 "collocates 'triftiger' and 'guter', without searching for collocates first."
Marc Kupietzffe28aa2026-09-05 17:50:45 +0200459 )
Marc Kupietz81ddc342026-09-03 13:18:53 +0200460 )
461
462 generated_code <- extract_r_code(call_llm_api(prompt, model, max_tokens = 300))
463
464 expect_true(grepl("KorAPConnection", generated_code), "Generated code should include KorAPConnection")
465 expect_true(grepl("collocationScoreQuery", generated_code), "Generated code should include collocationScoreQuery")
466 expect_true(grepl("triftiger", generated_code), "Generated code should include the collocate 'triftiger'")
467 expect_true(grepl("guter", generated_code), "Generated code should include the collocate 'guter'")
468 expect_true(test_code_syntax(generated_code), "Generated code should be syntactically valid R code")
469
470 cat("Generated association score code:\n", generated_code, "\n")
471 })
Marc Kupietz1e9f7912026-09-03 13:23:42 +0200472
Marc Kupietzdb075d52026-09-08 09:38:49 +0200473 test_that(paste(model, "can solve result caching task with README guidance"), {
474 skip_if_offline()
475 skip_if_no_api_key(model)
476 if (llmProvider(model)$name != "synthetic") skip_if_not_installed("tidyllm")
477 skip_if_not(!is.null(find_readme_path()), "Readme.md not found in current or parent directories")
478
479 prompt <- create_readme_prompt(
480 paste(
481 "write R code for an R Markdown document that reports how often 'Ameisenplage' occurs,",
482 "in a way that does not query the server again every time the document is knitted."
483 )
484 )
485
486 generated_code <- extract_r_code(call_llm_api(prompt, model, max_tokens = 300))
487
488 expect_true(grepl("KorAPConnection", generated_code), "Generated code should include KorAPConnection")
489 expect_true(grepl("cacheAs", generated_code), "Generated code should keep the result with cacheAs")
490 expect_true(grepl("Ameisenplage", generated_code), "Generated code should include the search term")
491 expect_true(test_code_syntax(generated_code), "Generated code should be syntactically valid R code")
492
493 cat("Generated result caching code:\n", generated_code, "\n")
494 })
495
496 test_that(paste(model, "can solve labelled corpora task with README guidance"), {
497 skip_if_offline()
498 skip_if_no_api_key(model)
499 if (llmProvider(model)$name != "synthetic") skip_if_not_installed("tidyllm")
500 skip_if_not(!is.null(find_readme_path()), "Readme.md not found in current or parent directories")
501
502 prompt <- create_readme_prompt(
503 paste(
504 "write R code that reports how many tokens the newspaper texts published before 2010 and",
505 "those published since 2010 contain, with the two rows labelled 'before' and 'since'."
506 )
507 )
508
509 generated_code <- extract_r_code(call_llm_api(prompt, model, max_tokens = 300))
510
511 expect_true(grepl("corpusStats", generated_code), "Generated code should include corpusStats")
512 # the labels come from the names of the vc vector, not from a column added afterwards
513 expect_true(
514 grepl("(vc\\s*=\\s*)?c\\(\\s*[`\"']?before[`\"']?\\s*=", generated_code),
515 "Generated code should name the virtual corpora in the vc vector"
516 )
517 expect_true(test_code_syntax(generated_code), "Generated code should be syntactically valid R code")
518
519 cat("Generated labelled corpora code:\n", generated_code, "\n")
520 })
521
Marc Kupietz1e9f7912026-09-03 13:23:42 +0200522 # The code of the following two tasks cannot reasonably be executed in a test:
523 # authorization needs a browser flow or a token for restricted data, and a
524 # multi-VC collocation analysis runs for minutes. Only the generated code is
525 # inspected, which is the point anyway: can the Readme be followed?
526
527 test_that(paste(model, "can solve authorization task with README guidance"), {
528 skip_if_offline()
529 skip_if_no_api_key(model)
530 if (llmProvider(model)$name != "synthetic") skip_if_not_installed("tidyllm")
531 skip_if_not(!is.null(find_readme_path()), "Readme.md not found in current or parent directories")
532
533 prompt <- create_readme_prompt(
534 paste(
535 "write R code that authorizes the application so that it also receives KWIC snippets from",
536 "corpora with restricted licenses, and then queries 'Ameisenplage' including those snippets."
Marc Kupietzffe28aa2026-09-05 17:50:45 +0200537 )
Marc Kupietz1e9f7912026-09-03 13:23:42 +0200538 )
539
540 generated_code <- extract_r_code(call_llm_api(prompt, model, max_tokens = 300))
541
542 expect_true(grepl("KorAPConnection", generated_code), "Generated code should include KorAPConnection")
543 expect_true(
544 grepl("auth\\(|accessToken", generated_code),
545 "Generated code should authorize via auth() or an accessToken"
546 )
547 expect_true(
548 grepl("metadataOnly\\s*=\\s*FALSE", generated_code),
549 "Generated code should set metadataOnly = FALSE to receive KWIC snippets"
550 )
551 expect_true(test_code_syntax(generated_code), "Generated code should be syntactically valid R code")
552
553 cat("Generated authorization code:\n", generated_code, "\n")
554 })
555
556 test_that(paste(model, "can solve multi-VC comparison task with README guidance"), {
557 skip_if_offline()
558 skip_if_no_api_key(model)
559 if (llmProvider(model)$name != "synthetic") skip_if_not_installed("tidyllm")
560 skip_if_not(!is.null(find_readme_path()), "Readme.md not found in current or parent directories")
561
562 prompt <- create_readme_prompt(
563 paste(
564 "write R code that compares the collocates of 'Kritik' between newspaper texts published before 2010",
565 "and those published since 2010, and shows those collocates that are attested in both, ordered by how",
566 "differently they are associated."
Marc Kupietzffe28aa2026-09-05 17:50:45 +0200567 )
Marc Kupietz1e9f7912026-09-03 13:23:42 +0200568 )
569
570 generated_code <- extract_r_code(call_llm_api(prompt, model, max_tokens = 500))
571
572 expect_true(grepl("collocationAnalysis", generated_code), "Generated code should include collocationAnalysis")
Marc Kupietz73e3f372026-09-05 15:30:52 +0200573 # the labels of the comparison columns come from the names of the vc vector,
574 # which may just as well be built before the call rather than inside it
Marc Kupietz1e9f7912026-09-03 13:23:42 +0200575 expect_true(
Marc Kupietz73e3f372026-09-05 15:30:52 +0200576 grepl("vc\\s*=\\s*c\\(\\s*[A-Za-z.`\"']", generated_code) ||
577 grepl("c\\(\\s*[`\"']?[A-Za-z.][A-Za-z0-9._]*[`\"']?\\s*=[^=]", generated_code),
Marc Kupietz1e9f7912026-09-03 13:23:42 +0200578 "Generated code should pass a named vector of virtual corpora"
579 )
580 # one row per collocate and vc, so the comparison needs to be reduced
581 expect_true(
582 grepl("label", generated_code) || grepl("distinct", generated_code),
583 "Generated code should reduce the result to one row per collocate, via label or distinct()"
584 )
585 # imputed scores describe presence/absence rather than a measured contrast
586 expect_true(grepl("imputed", generated_code), "Generated code should take the imputed flag into account")
587 expect_true(test_code_syntax(generated_code), "Generated code should be syntactically valid R code")
588
589 cat("Generated multi-VC comparison code:\n", generated_code, "\n")
590 })
Marc Kupietz323a4d32026-09-03 12:04:35 +0200591}