blob: 3454709fb9771d1116b8d7260e2eb7935ba7ed2f [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) {
177 # Remove markdown code blocks if present
178 code <- gsub("```[rR]?\\n?", "", response_text)
179 code <- gsub("```\\n?$", "", code)
180 # Remove leading/trailing whitespace
181 trimws(code)
182}
183
Marc Kupietze759b342025-07-05 19:48:20 +0200184# Helper function to test code syntax
185test_code_syntax <- function(code) {
Marc Kupietz2deadd82025-07-09 08:53:33 +0200186 tryCatch(
187 {
188 parse(text = code)
189 TRUE
190 },
191 error = function(e) {
192 cat("Syntax error:", as.character(e), "\n")
193 FALSE
194 }
195 )
Marc Kupietze759b342025-07-05 19:48:20 +0200196}
197
198# Helper function to run code if RUN_LLM_CODE is set
199run_code_if_enabled <- function(code, test_name) {
200 if (nzchar(Sys.getenv("RUN_LLM_CODE")) && Sys.getenv("RUN_LLM_CODE") == "true") {
201 cat("Running generated code for", test_name, "...\n")
Marc Kupietz2deadd82025-07-09 08:53:33 +0200202 tryCatch(
203 {
204 result <- eval(parse(text = code))
205 cat("Code executed successfully. Result type:", class(result), "\n")
206 if (is.data.frame(result)) {
207 cat("Result dimensions:", nrow(result), "rows,", ncol(result), "columns\n")
208 if (nrow(result) > 0) {
209 cat("First few rows:\n")
210 print(head(result, 3))
211 }
212 } else {
213 cat("Result preview:\n")
214 print(result)
Marc Kupietze759b342025-07-05 19:48:20 +0200215 }
Marc Kupietz2deadd82025-07-09 08:53:33 +0200216 return(TRUE)
217 },
218 error = function(e) {
219 cat("Runtime error:", as.character(e), "\n")
220 return(FALSE)
Marc Kupietze759b342025-07-05 19:48:20 +0200221 }
Marc Kupietz2deadd82025-07-09 08:53:33 +0200222 )
Marc Kupietze759b342025-07-05 19:48:20 +0200223 } else {
224 cat("Skipping code execution (set RUN_LLM_CODE=true to enable)\n")
225 return(NA)
226 }
227}
228
Marc Kupietz323a4d32026-09-03 12:04:35 +0200229for (model in llmModels()) {
230 test_that(paste(model, "can solve frequency query task with README guidance"), {
231 # Skip if offline
232 skip_if_offline()
Marc Kupietz2deadd82025-07-09 08:53:33 +0200233
Marc Kupietz323a4d32026-09-03 12:04:35 +0200234 # Skip if no API keys are set
235 skip_if_no_api_key(model)
Marc Kupietz2deadd82025-07-09 08:53:33 +0200236
Marc Kupietz323a4d32026-09-03 12:04:35 +0200237 # tidyllm is only suggested, so the tests must not fail without it
238 if (llmProvider(model)$name != "synthetic") skip_if_not_installed("tidyllm")
Marc Kupietz06143702025-07-05 17:49:31 +0200239
Marc Kupietz323a4d32026-09-03 12:04:35 +0200240 # Check for README file
241 skip_if_not(!is.null(find_readme_path()), "Readme.md not found in current or parent directories")
Marc Kupietz06143702025-07-05 17:49:31 +0200242
Marc Kupietz323a4d32026-09-03 12:04:35 +0200243 # Create the prompt with README context and task
244 prompt <- create_readme_prompt(
245 "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.",
246 "Write R code to query frequency of 'Demokratie' from the past three years using RKorAPClient."
247 )
Marc Kupietz06143702025-07-05 17:49:31 +0200248
Marc Kupietz323a4d32026-09-03 12:04:35 +0200249 # Call LLM API
250 generated_response <- call_llm_api(prompt, model, max_tokens = 500)
251 generated_code <- extract_r_code(generated_response)
Marc Kupietzb8839d32025-07-06 14:42:30 +0200252
Marc Kupietz323a4d32026-09-03 12:04:35 +0200253 # Basic checks on the generated code
254 expect_true(grepl("KorAPConnection", generated_code), "Generated code should include KorAPConnection")
255 expect_true(grepl("frequencyQuery", generated_code), "Generated code should include frequencyQuery")
256 expect_true(grepl("Demokratie", generated_code), "Generated code should include the search term 'Demokratie'")
257 last_year <- as.numeric(format(Sys.Date(), "%Y")) - 1
Marc Kupietz06143702025-07-05 17:49:31 +0200258
Marc Kupietz323a4d32026-09-03 12:04:35 +0200259 expect_true(grepl("Date in", generated_code), "Generated code should vc restriction on years")
Marc Kupietz06143702025-07-05 17:49:31 +0200260
Marc Kupietz323a4d32026-09-03 12:04:35 +0200261 # Check that the generated code contains essential RKorAPClient patterns
262 # expect_true(grepl("\\|>", generated_code) || grepl("%>%", generated_code), "Generated code should use pipe operators")
Marc Kupietz06143702025-07-05 17:49:31 +0200263
Marc Kupietz323a4d32026-09-03 12:04:35 +0200264 # Test code syntax
265 syntax_valid <- test_code_syntax(generated_code)
266 expect_true(syntax_valid, "Generated code should be syntactically valid R code")
Marc Kupietze759b342025-07-05 19:48:20 +0200267
Marc Kupietz323a4d32026-09-03 12:04:35 +0200268 # Print the generated code for manual inspection
269 cat("Generated code:\n", generated_code, "\n")
270
271 # Run the code if RUN_LLM_CODE is set
272 execution_result <- run_code_if_enabled(generated_code, "frequency query")
273 if (!is.na(execution_result)) {
274 expect_true(execution_result, "Generated code should execute without runtime errors")
275 }
276 })
Marc Kupietz06143702025-07-05 17:49:31 +0200277
Marc Kupietz345211a2025-07-06 12:52:24 +0200278
Marc Kupietz323a4d32026-09-03 12:04:35 +0200279 test_that(paste(model, "can solve collocation analysis task with README guidance"), {
280 # Skip if offline
281 skip_if_offline()
Marc Kupietz2deadd82025-07-09 08:53:33 +0200282
Marc Kupietz323a4d32026-09-03 12:04:35 +0200283 # Skip if no API keys are set
284 skip_if_no_api_key(model)
Marc Kupietz2deadd82025-07-09 08:53:33 +0200285
Marc Kupietz323a4d32026-09-03 12:04:35 +0200286 # tidyllm is only suggested, so the tests must not fail without it
287 if (llmProvider(model)$name != "synthetic") skip_if_not_installed("tidyllm")
Marc Kupietz06143702025-07-05 17:49:31 +0200288
Marc Kupietz323a4d32026-09-03 12:04:35 +0200289 # Check for README file
290 skip_if_not(!is.null(find_readme_path()), "Readme.md not found in current or parent directories")
Marc Kupietz06143702025-07-05 17:49:31 +0200291
Marc Kupietz323a4d32026-09-03 12:04:35 +0200292 # Create the prompt for collocation analysis
293 prompt <- create_readme_prompt(
294 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.
295 "),
296 "Write R code to perform collocation analysis for lemma 'leverage' using RKorAPClient."
297 )
Marc Kupietz06143702025-07-05 17:49:31 +0200298
Marc Kupietz323a4d32026-09-03 12:04:35 +0200299 # Call LLM API
300 generated_response <- call_llm_api(prompt, model, max_tokens = 500)
301 generated_code <- extract_r_code(generated_response)
Marc Kupietz06143702025-07-05 17:49:31 +0200302
Marc Kupietz323a4d32026-09-03 12:04:35 +0200303 # Basic checks on the generated code
304 expect_true(grepl("KorAPConnection", generated_code), "Generated code should include KorAPConnection")
305 expect_true(grepl("collocationAnalysis", generated_code), "Generated code should include collocationAnalysis")
Marc Kupietz3321dae2026-09-05 15:30:52 +0200306 # both ways of asking for the lemma are correct: the annotation layer in the
307 # query, as the Readme shows it, or collocationAnalysis' lemmatizeNodeQuery,
308 # which builds the same query from a plain word
309 expect_true(grepl("leverage", generated_code), "Generated code should include the node 'leverage'")
310 expect_true(
311 grepl("tt/l=leverage", generated_code) ||
312 grepl("lemmatizeNodeQuery\\s*=\\s*T", generated_code),
313 "Generated code should search for the lemma, via tt/l= or lemmatizeNodeQuery = TRUE"
314 )
Marc Kupietz323a4d32026-09-03 12:04:35 +0200315 # expect_true(grepl("auth", generated_code), "Generated code should include auth() for collocation analysis")
316 expect_true(grepl("instance/english", generated_code, fixed = TRUE), "Generated code should include the specified KorAP URL")
Marc Kupietze759b342025-07-05 19:48:20 +0200317
Marc Kupietz323a4d32026-09-03 12:04:35 +0200318 # Test code syntax
319 syntax_valid <- test_code_syntax(generated_code)
320 expect_true(syntax_valid, "Generated code should be syntactically valid R code")
Marc Kupietze759b342025-07-05 19:48:20 +0200321
Marc Kupietz323a4d32026-09-03 12:04:35 +0200322 # Print the generated code for manual inspection
323 cat("Generated collocation analysis code:\n", generated_code, "\n")
Marc Kupietz06143702025-07-05 17:49:31 +0200324
Marc Kupietz323a4d32026-09-03 12:04:35 +0200325 # Run the code if RUN_LLM_CODE is set
326 execution_result <- run_code_if_enabled(generated_code, "collocation analysis")
327 if (!is.na(execution_result)) {
328 expect_true(execution_result, "Generated code should execute without runtime errors")
329 }
330 })
Marc Kupietz2deadd82025-07-09 08:53:33 +0200331
Marc Kupietz323a4d32026-09-03 12:04:35 +0200332 test_that(paste(model, "can solve corpus query task with README guidance"), {
333 # Skip if offline
334 skip_if_offline()
Marc Kupietz2deadd82025-07-09 08:53:33 +0200335
Marc Kupietz323a4d32026-09-03 12:04:35 +0200336 # Skip if no API keys are set
337 skip_if_no_api_key(model)
Marc Kupietz06143702025-07-05 17:49:31 +0200338
Marc Kupietz323a4d32026-09-03 12:04:35 +0200339 # tidyllm is only suggested, so the tests must not fail without it
340 if (llmProvider(model)$name != "synthetic") skip_if_not_installed("tidyllm")
Marc Kupietz06143702025-07-05 17:49:31 +0200341
Marc Kupietz323a4d32026-09-03 12:04:35 +0200342 # Check for README file
343 skip_if_not(!is.null(find_readme_path()), "Readme.md not found in current or parent directories")
Marc Kupietz06143702025-07-05 17:49:31 +0200344
Marc Kupietz323a4d32026-09-03 12:04:35 +0200345 # Create the prompt for corpus query
346 prompt <- create_readme_prompt(
347 "write R code to perform a simple corpus query for 'Hello world' and fetch all results. The code should use the RKorAPClient package.",
348 "Write R code to query 'Hello world' and fetch all results using RKorAPClient."
349 )
Marc Kupietz06143702025-07-05 17:49:31 +0200350
Marc Kupietz323a4d32026-09-03 12:04:35 +0200351 # Call LLM API
352 generated_response <- call_llm_api(prompt, model, max_tokens = 300)
353 generated_code <- extract_r_code(generated_response)
Marc Kupietz06143702025-07-05 17:49:31 +0200354
Marc Kupietz323a4d32026-09-03 12:04:35 +0200355 # Basic checks on the generated code
356 expect_true(grepl("KorAPConnection", generated_code), "Generated code should include KorAPConnection")
357 expect_true(grepl("corpusQuery", generated_code), "Generated code should include corpusQuery")
358 expect_true(grepl("Hello world", generated_code), "Generated code should include the search term 'Hello world'")
359 expect_true(grepl("fetchAll", generated_code), "Generated code should include fetchAll")
Marc Kupietze759b342025-07-05 19:48:20 +0200360
Marc Kupietz323a4d32026-09-03 12:04:35 +0200361 # Check that the generated code follows the README example pattern
362 expect_true(
363 grepl("\\|>", generated_code) || grepl("%>%", generated_code),
364 "Generated code should use pipe operators"
365 )
Marc Kupietze759b342025-07-05 19:48:20 +0200366
Marc Kupietz323a4d32026-09-03 12:04:35 +0200367 # Test code syntax
368 syntax_valid <- test_code_syntax(generated_code)
369 expect_true(syntax_valid, "Generated code should be syntactically valid R code")
370
371 # Print the generated code for manual inspection
372 cat("Generated corpus query code:\n", generated_code, "\n")
373
374 # Run the code if RUN_LLM_CODE is set
375 execution_result <- run_code_if_enabled(generated_code, "corpus query")
376 if (!is.na(execution_result)) {
377 expect_true(execution_result, "Generated code should execute without runtime errors")
378 }
379 })
Marc Kupietz81ddc342026-09-03 13:18:53 +0200380
381 test_that(paste(model, "can solve corpus size task with README guidance"), {
382 skip_if_offline()
383 skip_if_no_api_key(model)
384 if (llmProvider(model)$name != "synthetic") skip_if_not_installed("tidyllm")
385 skip_if_not(!is.null(find_readme_path()), "Readme.md not found in current or parent directories")
386
387 prompt <- create_readme_prompt(
388 "write R code that reports how many tokens the virtual corpus of newspaper texts published since 2020 contains.",
389 "Write R code to determine the size of a virtual corpus using RKorAPClient."
390 )
391
392 generated_code <- extract_r_code(call_llm_api(prompt, model, max_tokens = 300))
393
394 expect_true(grepl("KorAPConnection", generated_code), "Generated code should include KorAPConnection")
395 expect_true(grepl("corpusStats", generated_code), "Generated code should include corpusStats")
396 expect_true(grepl("vc", generated_code), "Generated code should restrict to a virtual corpus")
397 expect_true(test_code_syntax(generated_code), "Generated code should be syntactically valid R code")
398
399 cat("Generated corpus size code:\n", generated_code, "\n")
400 })
401
402 test_that(paste(model, "can solve text metadata task with README guidance"), {
403 skip_if_offline()
404 skip_if_no_api_key(model)
405 if (llmProvider(model)$name != "synthetic") skip_if_not_installed("tidyllm")
406 skip_if_not(!is.null(find_readme_path()), "Readme.md not found in current or parent directories")
407
408 prompt <- create_readme_prompt(
409 "write R code that retrieves all metadata KorAP holds for the text with the sigle WPD17/L79/98721.",
410 "Write R code to retrieve the metadata of a text using RKorAPClient."
411 )
412
413 generated_code <- extract_r_code(call_llm_api(prompt, model, max_tokens = 300))
414
415 expect_true(grepl("KorAPConnection", generated_code), "Generated code should include KorAPConnection")
416 expect_true(grepl("textMetadata", generated_code), "Generated code should include textMetadata")
417 expect_true(grepl("WPD17/L79/98721", generated_code, fixed = TRUE), "Generated code should include the text sigle")
418 expect_true(test_code_syntax(generated_code), "Generated code should be syntactically valid R code")
419
420 cat("Generated text metadata code:\n", generated_code, "\n")
421 })
422
423 test_that(paste(model, "can solve association score task with README guidance"), {
424 skip_if_offline()
425 skip_if_no_api_key(model)
426 if (llmProvider(model)$name != "synthetic") skip_if_not_installed("tidyllm")
427 skip_if_not(!is.null(find_readme_path()), "Readme.md not found in current or parent directories")
428
429 prompt <- create_readme_prompt(
430 paste(
431 "write R code that computes association scores for the word 'Grund' together with each of the",
432 "collocates 'triftiger' and 'guter', without searching for collocates first."
433 ),
434 "Write R code to compute association scores for known collocation candidates using RKorAPClient."
435 )
436
437 generated_code <- extract_r_code(call_llm_api(prompt, model, max_tokens = 300))
438
439 expect_true(grepl("KorAPConnection", generated_code), "Generated code should include KorAPConnection")
440 expect_true(grepl("collocationScoreQuery", generated_code), "Generated code should include collocationScoreQuery")
441 expect_true(grepl("triftiger", generated_code), "Generated code should include the collocate 'triftiger'")
442 expect_true(grepl("guter", generated_code), "Generated code should include the collocate 'guter'")
443 expect_true(test_code_syntax(generated_code), "Generated code should be syntactically valid R code")
444
445 cat("Generated association score code:\n", generated_code, "\n")
446 })
Marc Kupietz1e9f7912026-09-03 13:23:42 +0200447
448 # The code of the following two tasks cannot reasonably be executed in a test:
449 # authorization needs a browser flow or a token for restricted data, and a
450 # multi-VC collocation analysis runs for minutes. Only the generated code is
451 # inspected, which is the point anyway: can the Readme be followed?
452
453 test_that(paste(model, "can solve authorization task with README guidance"), {
454 skip_if_offline()
455 skip_if_no_api_key(model)
456 if (llmProvider(model)$name != "synthetic") skip_if_not_installed("tidyllm")
457 skip_if_not(!is.null(find_readme_path()), "Readme.md not found in current or parent directories")
458
459 prompt <- create_readme_prompt(
460 paste(
461 "write R code that authorizes the application so that it also receives KWIC snippets from",
462 "corpora with restricted licenses, and then queries 'Ameisenplage' including those snippets."
463 ),
464 "Write R code that authorizes and retrieves KWIC snippets using RKorAPClient."
465 )
466
467 generated_code <- extract_r_code(call_llm_api(prompt, model, max_tokens = 300))
468
469 expect_true(grepl("KorAPConnection", generated_code), "Generated code should include KorAPConnection")
470 expect_true(
471 grepl("auth\\(|accessToken", generated_code),
472 "Generated code should authorize via auth() or an accessToken"
473 )
474 expect_true(
475 grepl("metadataOnly\\s*=\\s*FALSE", generated_code),
476 "Generated code should set metadataOnly = FALSE to receive KWIC snippets"
477 )
478 expect_true(test_code_syntax(generated_code), "Generated code should be syntactically valid R code")
479
480 cat("Generated authorization code:\n", generated_code, "\n")
481 })
482
483 test_that(paste(model, "can solve multi-VC comparison task with README guidance"), {
484 skip_if_offline()
485 skip_if_no_api_key(model)
486 if (llmProvider(model)$name != "synthetic") skip_if_not_installed("tidyllm")
487 skip_if_not(!is.null(find_readme_path()), "Readme.md not found in current or parent directories")
488
489 prompt <- create_readme_prompt(
490 paste(
491 "write R code that compares the collocates of 'Kritik' between newspaper texts published before 2010",
492 "and those published since 2010, and shows those collocates that are attested in both, ordered by how",
493 "differently they are associated."
494 ),
495 "Write R code comparing collocates across two virtual corpora using RKorAPClient."
496 )
497
498 generated_code <- extract_r_code(call_llm_api(prompt, model, max_tokens = 500))
499
500 expect_true(grepl("collocationAnalysis", generated_code), "Generated code should include collocationAnalysis")
501 # the labels of the comparison columns come from the names of the vc vector
502 expect_true(
503 grepl("vc\\s*=\\s*c\\(\\s*[A-Za-z.`\"']", generated_code),
504 "Generated code should pass a named vector of virtual corpora"
505 )
506 # one row per collocate and vc, so the comparison needs to be reduced
507 expect_true(
508 grepl("label", generated_code) || grepl("distinct", generated_code),
509 "Generated code should reduce the result to one row per collocate, via label or distinct()"
510 )
511 # imputed scores describe presence/absence rather than a measured contrast
512 expect_true(grepl("imputed", generated_code), "Generated code should take the imputed flag into account")
513 expect_true(test_code_syntax(generated_code), "Generated code should be syntactically valid R code")
514
515 cat("Generated multi-VC comparison code:\n", generated_code, "\n")
516 })
Marc Kupietz323a4d32026-09-03 12:04:35 +0200517}