blob: f547c042a532b937901fda68b489e1e8c03c2a2f [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
159create_readme_prompt <- function(task_description, specific_task) {
160 readme_text <- read_readme_content()
161 if (is.null(readme_text)) {
162 stop("README.md not found")
163 }
164
165 paste0(
166 "You are an expert R programmer. Based on the following README documentation for the RKorAPClient package, ",
167 task_description, "\n\n",
168 "README Documentation:\n",
169 readme_text,
170 "\n\nTask: ", specific_task,
171 "\n\nProvide only the R code without explanations."
172 )
173}
174
175# Helper function to extract R code from markdown code blocks
176extract_r_code <- function(response_text) {
Marc Kupietz08cc2e52026-09-05 16:49:25 +0200177 # Asked for code alone, a model may still explain itself around it, or offer a
178 # second way of doing the same in a block of its own. Only the first block is
179 # what was asked for; stripping the fences and keeping everything else puts
180 # the prose in between into the code, where it does not parse.
181 block <- stringr::str_match(response_text, "(?s)```[^\\n]*\\n(.*?)```")[1, 2]
182 trimws(if (is.na(block)) response_text else block)
Marc Kupietz06143702025-07-05 17:49:31 +0200183}
184
Marc Kupietz08cc2e52026-09-05 16:49:25 +0200185test_that("the first code block is what is taken from a reply", {
186 fenced <- function(...) paste(c(...), collapse = "\n")
187
188 expect_equal(
189 extract_r_code(fenced("```r", "corpusStats(kco)", "```")),
190 "corpusStats(kco)"
191 )
192 # prose around the block does not belong to the code
193 expect_equal(
194 extract_r_code(fenced("Here you are:", "```R", "corpusStats(kco)", "```", "Hope that helps!")),
195 "corpusStats(kco)"
196 )
197 # a second block, offered as an alternative, would not parse together with the
198 # sentence introducing it
199 expect_equal(
200 extract_r_code(fenced(
201 "```r", "corpusStats(kco)", "```",
202 "Or, as a data frame:",
203 "```r", "corpusStats(kco, as.df = TRUE)", "```"
204 )),
205 "corpusStats(kco)"
206 )
207 # a reply that took "only the R code" literally has no fences to look for
208 expect_equal(extract_r_code("corpusStats(kco)"), "corpusStats(kco)")
209})
210
Marc Kupietze759b342025-07-05 19:48:20 +0200211# Helper function to test code syntax
212test_code_syntax <- function(code) {
Marc Kupietz2deadd82025-07-09 08:53:33 +0200213 tryCatch(
214 {
215 parse(text = code)
216 TRUE
217 },
218 error = function(e) {
219 cat("Syntax error:", as.character(e), "\n")
220 FALSE
221 }
222 )
Marc Kupietze759b342025-07-05 19:48:20 +0200223}
224
225# Helper function to run code if RUN_LLM_CODE is set
226run_code_if_enabled <- function(code, test_name) {
227 if (nzchar(Sys.getenv("RUN_LLM_CODE")) && Sys.getenv("RUN_LLM_CODE") == "true") {
228 cat("Running generated code for", test_name, "...\n")
Marc Kupietz2deadd82025-07-09 08:53:33 +0200229 tryCatch(
230 {
231 result <- eval(parse(text = code))
232 cat("Code executed successfully. Result type:", class(result), "\n")
233 if (is.data.frame(result)) {
234 cat("Result dimensions:", nrow(result), "rows,", ncol(result), "columns\n")
235 if (nrow(result) > 0) {
236 cat("First few rows:\n")
237 print(head(result, 3))
238 }
239 } else {
240 cat("Result preview:\n")
241 print(result)
Marc Kupietze759b342025-07-05 19:48:20 +0200242 }
Marc Kupietz2deadd82025-07-09 08:53:33 +0200243 return(TRUE)
244 },
245 error = function(e) {
246 cat("Runtime error:", as.character(e), "\n")
247 return(FALSE)
Marc Kupietze759b342025-07-05 19:48:20 +0200248 }
Marc Kupietz2deadd82025-07-09 08:53:33 +0200249 )
Marc Kupietze759b342025-07-05 19:48:20 +0200250 } else {
251 cat("Skipping code execution (set RUN_LLM_CODE=true to enable)\n")
252 return(NA)
253 }
254}
255
Marc Kupietz323a4d32026-09-03 12:04:35 +0200256for (model in llmModels()) {
257 test_that(paste(model, "can solve frequency query task with README guidance"), {
258 # Skip if offline
259 skip_if_offline()
Marc Kupietz2deadd82025-07-09 08:53:33 +0200260
Marc Kupietz323a4d32026-09-03 12:04:35 +0200261 # Skip if no API keys are set
262 skip_if_no_api_key(model)
Marc Kupietz2deadd82025-07-09 08:53:33 +0200263
Marc Kupietz323a4d32026-09-03 12:04:35 +0200264 # tidyllm is only suggested, so the tests must not fail without it
265 if (llmProvider(model)$name != "synthetic") skip_if_not_installed("tidyllm")
Marc Kupietz06143702025-07-05 17:49:31 +0200266
Marc Kupietz323a4d32026-09-03 12:04:35 +0200267 # Check for README file
268 skip_if_not(!is.null(find_readme_path()), "Readme.md not found in current or parent directories")
Marc Kupietz06143702025-07-05 17:49:31 +0200269
Marc Kupietz323a4d32026-09-03 12:04:35 +0200270 # Create the prompt with README context and task
271 prompt <- create_readme_prompt(
272 "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.",
273 "Write R code to query frequency of 'Demokratie' from the past three years using RKorAPClient."
274 )
Marc Kupietz06143702025-07-05 17:49:31 +0200275
Marc Kupietz323a4d32026-09-03 12:04:35 +0200276 # Call LLM API
277 generated_response <- call_llm_api(prompt, model, max_tokens = 500)
278 generated_code <- extract_r_code(generated_response)
Marc Kupietzb8839d32025-07-06 14:42:30 +0200279
Marc Kupietz323a4d32026-09-03 12:04:35 +0200280 # Basic checks on the generated code
281 expect_true(grepl("KorAPConnection", generated_code), "Generated code should include KorAPConnection")
282 expect_true(grepl("frequencyQuery", generated_code), "Generated code should include frequencyQuery")
283 expect_true(grepl("Demokratie", generated_code), "Generated code should include the search term 'Demokratie'")
284 last_year <- as.numeric(format(Sys.Date(), "%Y")) - 1
Marc Kupietz06143702025-07-05 17:49:31 +0200285
Marc Kupietz323a4d32026-09-03 12:04:35 +0200286 expect_true(grepl("Date in", generated_code), "Generated code should vc restriction on years")
Marc Kupietz06143702025-07-05 17:49:31 +0200287
Marc Kupietz323a4d32026-09-03 12:04:35 +0200288 # Check that the generated code contains essential RKorAPClient patterns
289 # expect_true(grepl("\\|>", generated_code) || grepl("%>%", generated_code), "Generated code should use pipe operators")
Marc Kupietz06143702025-07-05 17:49:31 +0200290
Marc Kupietz323a4d32026-09-03 12:04:35 +0200291 # Test code syntax
292 syntax_valid <- test_code_syntax(generated_code)
293 expect_true(syntax_valid, "Generated code should be syntactically valid R code")
Marc Kupietze759b342025-07-05 19:48:20 +0200294
Marc Kupietz323a4d32026-09-03 12:04:35 +0200295 # Print the generated code for manual inspection
296 cat("Generated code:\n", generated_code, "\n")
297
298 # Run the code if RUN_LLM_CODE is set
299 execution_result <- run_code_if_enabled(generated_code, "frequency query")
300 if (!is.na(execution_result)) {
301 expect_true(execution_result, "Generated code should execute without runtime errors")
302 }
303 })
Marc Kupietz06143702025-07-05 17:49:31 +0200304
Marc Kupietz345211a2025-07-06 12:52:24 +0200305
Marc Kupietz323a4d32026-09-03 12:04:35 +0200306 test_that(paste(model, "can solve collocation analysis task with README guidance"), {
307 # Skip if offline
308 skip_if_offline()
Marc Kupietz2deadd82025-07-09 08:53:33 +0200309
Marc Kupietz323a4d32026-09-03 12:04:35 +0200310 # Skip if no API keys are set
311 skip_if_no_api_key(model)
Marc Kupietz2deadd82025-07-09 08:53:33 +0200312
Marc Kupietz323a4d32026-09-03 12:04:35 +0200313 # tidyllm is only suggested, so the tests must not fail without it
314 if (llmProvider(model)$name != "synthetic") skip_if_not_installed("tidyllm")
Marc Kupietz06143702025-07-05 17:49:31 +0200315
Marc Kupietz323a4d32026-09-03 12:04:35 +0200316 # Check for README file
317 skip_if_not(!is.null(find_readme_path()), "Readme.md not found in current or parent directories")
Marc Kupietz06143702025-07-05 17:49:31 +0200318
Marc Kupietz323a4d32026-09-03 12:04:35 +0200319 # Create the prompt for collocation analysis
320 prompt <- create_readme_prompt(
321 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.
322 "),
323 "Write R code to perform collocation analysis for lemma 'leverage' using RKorAPClient."
324 )
Marc Kupietz06143702025-07-05 17:49:31 +0200325
Marc Kupietz323a4d32026-09-03 12:04:35 +0200326 # Call LLM API
327 generated_response <- call_llm_api(prompt, model, max_tokens = 500)
328 generated_code <- extract_r_code(generated_response)
Marc Kupietz06143702025-07-05 17:49:31 +0200329
Marc Kupietz323a4d32026-09-03 12:04:35 +0200330 # Basic checks on the generated code
331 expect_true(grepl("KorAPConnection", generated_code), "Generated code should include KorAPConnection")
332 expect_true(grepl("collocationAnalysis", generated_code), "Generated code should include collocationAnalysis")
Marc Kupietz3321dae2026-09-05 15:30:52 +0200333 # both ways of asking for the lemma are correct: the annotation layer in the
334 # query, as the Readme shows it, or collocationAnalysis' lemmatizeNodeQuery,
335 # which builds the same query from a plain word
336 expect_true(grepl("leverage", generated_code), "Generated code should include the node 'leverage'")
337 expect_true(
338 grepl("tt/l=leverage", generated_code) ||
339 grepl("lemmatizeNodeQuery\\s*=\\s*T", generated_code),
340 "Generated code should search for the lemma, via tt/l= or lemmatizeNodeQuery = TRUE"
341 )
Marc Kupietz323a4d32026-09-03 12:04:35 +0200342 # expect_true(grepl("auth", generated_code), "Generated code should include auth() for collocation analysis")
343 expect_true(grepl("instance/english", generated_code, fixed = TRUE), "Generated code should include the specified KorAP URL")
Marc Kupietze759b342025-07-05 19:48:20 +0200344
Marc Kupietz323a4d32026-09-03 12:04:35 +0200345 # Test code syntax
346 syntax_valid <- test_code_syntax(generated_code)
347 expect_true(syntax_valid, "Generated code should be syntactically valid R code")
Marc Kupietze759b342025-07-05 19:48:20 +0200348
Marc Kupietz323a4d32026-09-03 12:04:35 +0200349 # Print the generated code for manual inspection
350 cat("Generated collocation analysis code:\n", generated_code, "\n")
Marc Kupietz06143702025-07-05 17:49:31 +0200351
Marc Kupietz323a4d32026-09-03 12:04:35 +0200352 # Run the code if RUN_LLM_CODE is set
353 execution_result <- run_code_if_enabled(generated_code, "collocation analysis")
354 if (!is.na(execution_result)) {
355 expect_true(execution_result, "Generated code should execute without runtime errors")
356 }
357 })
Marc Kupietz2deadd82025-07-09 08:53:33 +0200358
Marc Kupietz323a4d32026-09-03 12:04:35 +0200359 test_that(paste(model, "can solve corpus query task with README guidance"), {
360 # Skip if offline
361 skip_if_offline()
Marc Kupietz2deadd82025-07-09 08:53:33 +0200362
Marc Kupietz323a4d32026-09-03 12:04:35 +0200363 # Skip if no API keys are set
364 skip_if_no_api_key(model)
Marc Kupietz06143702025-07-05 17:49:31 +0200365
Marc Kupietz323a4d32026-09-03 12:04:35 +0200366 # tidyllm is only suggested, so the tests must not fail without it
367 if (llmProvider(model)$name != "synthetic") skip_if_not_installed("tidyllm")
Marc Kupietz06143702025-07-05 17:49:31 +0200368
Marc Kupietz323a4d32026-09-03 12:04:35 +0200369 # Check for README file
370 skip_if_not(!is.null(find_readme_path()), "Readme.md not found in current or parent directories")
Marc Kupietz06143702025-07-05 17:49:31 +0200371
Marc Kupietz323a4d32026-09-03 12:04:35 +0200372 # Create the prompt for corpus query
373 prompt <- create_readme_prompt(
374 "write R code to perform a simple corpus query for 'Hello world' and fetch all results. The code should use the RKorAPClient package.",
375 "Write R code to query 'Hello world' and fetch all results using RKorAPClient."
376 )
Marc Kupietz06143702025-07-05 17:49:31 +0200377
Marc Kupietz323a4d32026-09-03 12:04:35 +0200378 # Call LLM API
379 generated_response <- call_llm_api(prompt, model, max_tokens = 300)
380 generated_code <- extract_r_code(generated_response)
Marc Kupietz06143702025-07-05 17:49:31 +0200381
Marc Kupietz323a4d32026-09-03 12:04:35 +0200382 # Basic checks on the generated code
383 expect_true(grepl("KorAPConnection", generated_code), "Generated code should include KorAPConnection")
384 expect_true(grepl("corpusQuery", generated_code), "Generated code should include corpusQuery")
385 expect_true(grepl("Hello world", generated_code), "Generated code should include the search term 'Hello world'")
386 expect_true(grepl("fetchAll", generated_code), "Generated code should include fetchAll")
Marc Kupietze759b342025-07-05 19:48:20 +0200387
Marc Kupietz323a4d32026-09-03 12:04:35 +0200388 # Check that the generated code follows the README example pattern
389 expect_true(
390 grepl("\\|>", generated_code) || grepl("%>%", generated_code),
391 "Generated code should use pipe operators"
392 )
Marc Kupietze759b342025-07-05 19:48:20 +0200393
Marc Kupietz323a4d32026-09-03 12:04:35 +0200394 # Test code syntax
395 syntax_valid <- test_code_syntax(generated_code)
396 expect_true(syntax_valid, "Generated code should be syntactically valid R code")
397
398 # Print the generated code for manual inspection
399 cat("Generated corpus query code:\n", generated_code, "\n")
400
401 # Run the code if RUN_LLM_CODE is set
402 execution_result <- run_code_if_enabled(generated_code, "corpus query")
403 if (!is.na(execution_result)) {
404 expect_true(execution_result, "Generated code should execute without runtime errors")
405 }
406 })
Marc Kupietz81ddc342026-09-03 13:18:53 +0200407
408 test_that(paste(model, "can solve corpus size task with README guidance"), {
409 skip_if_offline()
410 skip_if_no_api_key(model)
411 if (llmProvider(model)$name != "synthetic") skip_if_not_installed("tidyllm")
412 skip_if_not(!is.null(find_readme_path()), "Readme.md not found in current or parent directories")
413
414 prompt <- create_readme_prompt(
415 "write R code that reports how many tokens the virtual corpus of newspaper texts published since 2020 contains.",
416 "Write R code to determine the size of a virtual corpus using RKorAPClient."
417 )
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(
436 "write R code that retrieves all metadata KorAP holds for the text with the sigle WPD17/L79/98721.",
437 "Write R code to retrieve the metadata of a text using RKorAPClient."
438 )
439
440 generated_code <- extract_r_code(call_llm_api(prompt, model, max_tokens = 300))
441
442 expect_true(grepl("KorAPConnection", generated_code), "Generated code should include KorAPConnection")
443 expect_true(grepl("textMetadata", generated_code), "Generated code should include textMetadata")
444 expect_true(grepl("WPD17/L79/98721", generated_code, fixed = TRUE), "Generated code should include the text sigle")
445 expect_true(test_code_syntax(generated_code), "Generated code should be syntactically valid R code")
446
447 cat("Generated text metadata code:\n", generated_code, "\n")
448 })
449
450 test_that(paste(model, "can solve association score task with README guidance"), {
451 skip_if_offline()
452 skip_if_no_api_key(model)
453 if (llmProvider(model)$name != "synthetic") skip_if_not_installed("tidyllm")
454 skip_if_not(!is.null(find_readme_path()), "Readme.md not found in current or parent directories")
455
456 prompt <- create_readme_prompt(
457 paste(
458 "write R code that computes association scores for the word 'Grund' together with each of the",
459 "collocates 'triftiger' and 'guter', without searching for collocates first."
460 ),
461 "Write R code to compute association scores for known collocation candidates using RKorAPClient."
462 )
463
464 generated_code <- extract_r_code(call_llm_api(prompt, model, max_tokens = 300))
465
466 expect_true(grepl("KorAPConnection", generated_code), "Generated code should include KorAPConnection")
467 expect_true(grepl("collocationScoreQuery", generated_code), "Generated code should include collocationScoreQuery")
468 expect_true(grepl("triftiger", generated_code), "Generated code should include the collocate 'triftiger'")
469 expect_true(grepl("guter", generated_code), "Generated code should include the collocate 'guter'")
470 expect_true(test_code_syntax(generated_code), "Generated code should be syntactically valid R code")
471
472 cat("Generated association score code:\n", generated_code, "\n")
473 })
Marc Kupietz1e9f7912026-09-03 13:23:42 +0200474
475 # The code of the following two tasks cannot reasonably be executed in a test:
476 # authorization needs a browser flow or a token for restricted data, and a
477 # multi-VC collocation analysis runs for minutes. Only the generated code is
478 # inspected, which is the point anyway: can the Readme be followed?
479
480 test_that(paste(model, "can solve authorization task with README guidance"), {
481 skip_if_offline()
482 skip_if_no_api_key(model)
483 if (llmProvider(model)$name != "synthetic") skip_if_not_installed("tidyllm")
484 skip_if_not(!is.null(find_readme_path()), "Readme.md not found in current or parent directories")
485
486 prompt <- create_readme_prompt(
487 paste(
488 "write R code that authorizes the application so that it also receives KWIC snippets from",
489 "corpora with restricted licenses, and then queries 'Ameisenplage' including those snippets."
490 ),
491 "Write R code that authorizes and retrieves KWIC snippets using RKorAPClient."
492 )
493
494 generated_code <- extract_r_code(call_llm_api(prompt, model, max_tokens = 300))
495
496 expect_true(grepl("KorAPConnection", generated_code), "Generated code should include KorAPConnection")
497 expect_true(
498 grepl("auth\\(|accessToken", generated_code),
499 "Generated code should authorize via auth() or an accessToken"
500 )
501 expect_true(
502 grepl("metadataOnly\\s*=\\s*FALSE", generated_code),
503 "Generated code should set metadataOnly = FALSE to receive KWIC snippets"
504 )
505 expect_true(test_code_syntax(generated_code), "Generated code should be syntactically valid R code")
506
507 cat("Generated authorization code:\n", generated_code, "\n")
508 })
509
510 test_that(paste(model, "can solve multi-VC comparison task with README guidance"), {
511 skip_if_offline()
512 skip_if_no_api_key(model)
513 if (llmProvider(model)$name != "synthetic") skip_if_not_installed("tidyllm")
514 skip_if_not(!is.null(find_readme_path()), "Readme.md not found in current or parent directories")
515
516 prompt <- create_readme_prompt(
517 paste(
518 "write R code that compares the collocates of 'Kritik' between newspaper texts published before 2010",
519 "and those published since 2010, and shows those collocates that are attested in both, ordered by how",
520 "differently they are associated."
521 ),
522 "Write R code comparing collocates across two virtual corpora using RKorAPClient."
523 )
524
525 generated_code <- extract_r_code(call_llm_api(prompt, model, max_tokens = 500))
526
527 expect_true(grepl("collocationAnalysis", generated_code), "Generated code should include collocationAnalysis")
Marc Kupietz73e3f372026-09-05 15:30:52 +0200528 # the labels of the comparison columns come from the names of the vc vector,
529 # which may just as well be built before the call rather than inside it
Marc Kupietz1e9f7912026-09-03 13:23:42 +0200530 expect_true(
Marc Kupietz73e3f372026-09-05 15:30:52 +0200531 grepl("vc\\s*=\\s*c\\(\\s*[A-Za-z.`\"']", generated_code) ||
532 grepl("c\\(\\s*[`\"']?[A-Za-z.][A-Za-z0-9._]*[`\"']?\\s*=[^=]", generated_code),
Marc Kupietz1e9f7912026-09-03 13:23:42 +0200533 "Generated code should pass a named vector of virtual corpora"
534 )
535 # one row per collocate and vc, so the comparison needs to be reduced
536 expect_true(
537 grepl("label", generated_code) || grepl("distinct", generated_code),
538 "Generated code should reduce the result to one row per collocate, via label or distinct()"
539 )
540 # imputed scores describe presence/absence rather than a measured contrast
541 expect_true(grepl("imputed", generated_code), "Generated code should take the imputed flag into account")
542 expect_true(test_code_syntax(generated_code), "Generated code should be syntactically valid R code")
543
544 cat("Generated multi-VC comparison code:\n", generated_code, "\n")
545 })
Marc Kupietz323a4d32026-09-03 12:04:35 +0200546}