blob: f44752322b6fc4dd9a02ef5a22e95885412601a0 [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")
306 expect_true(grepl("tt/l=leverage", generated_code), "Generated code should include the search the lemma 'leverage'")
307 # expect_true(grepl("auth", generated_code), "Generated code should include auth() for collocation analysis")
308 expect_true(grepl("instance/english", generated_code, fixed = TRUE), "Generated code should include the specified KorAP URL")
Marc Kupietze759b342025-07-05 19:48:20 +0200309
Marc Kupietz323a4d32026-09-03 12:04:35 +0200310 # Test code syntax
311 syntax_valid <- test_code_syntax(generated_code)
312 expect_true(syntax_valid, "Generated code should be syntactically valid R code")
Marc Kupietze759b342025-07-05 19:48:20 +0200313
Marc Kupietz323a4d32026-09-03 12:04:35 +0200314 # Print the generated code for manual inspection
315 cat("Generated collocation analysis code:\n", generated_code, "\n")
Marc Kupietz06143702025-07-05 17:49:31 +0200316
Marc Kupietz323a4d32026-09-03 12:04:35 +0200317 # Run the code if RUN_LLM_CODE is set
318 execution_result <- run_code_if_enabled(generated_code, "collocation analysis")
319 if (!is.na(execution_result)) {
320 expect_true(execution_result, "Generated code should execute without runtime errors")
321 }
322 })
Marc Kupietz2deadd82025-07-09 08:53:33 +0200323
Marc Kupietz323a4d32026-09-03 12:04:35 +0200324 test_that(paste(model, "can solve corpus query task with README guidance"), {
325 # Skip if offline
326 skip_if_offline()
Marc Kupietz2deadd82025-07-09 08:53:33 +0200327
Marc Kupietz323a4d32026-09-03 12:04:35 +0200328 # Skip if no API keys are set
329 skip_if_no_api_key(model)
Marc Kupietz06143702025-07-05 17:49:31 +0200330
Marc Kupietz323a4d32026-09-03 12:04:35 +0200331 # tidyllm is only suggested, so the tests must not fail without it
332 if (llmProvider(model)$name != "synthetic") skip_if_not_installed("tidyllm")
Marc Kupietz06143702025-07-05 17:49:31 +0200333
Marc Kupietz323a4d32026-09-03 12:04:35 +0200334 # Check for README file
335 skip_if_not(!is.null(find_readme_path()), "Readme.md not found in current or parent directories")
Marc Kupietz06143702025-07-05 17:49:31 +0200336
Marc Kupietz323a4d32026-09-03 12:04:35 +0200337 # Create the prompt for corpus query
338 prompt <- create_readme_prompt(
339 "write R code to perform a simple corpus query for 'Hello world' and fetch all results. The code should use the RKorAPClient package.",
340 "Write R code to query 'Hello world' and fetch all results using RKorAPClient."
341 )
Marc Kupietz06143702025-07-05 17:49:31 +0200342
Marc Kupietz323a4d32026-09-03 12:04:35 +0200343 # Call LLM API
344 generated_response <- call_llm_api(prompt, model, max_tokens = 300)
345 generated_code <- extract_r_code(generated_response)
Marc Kupietz06143702025-07-05 17:49:31 +0200346
Marc Kupietz323a4d32026-09-03 12:04:35 +0200347 # Basic checks on the generated code
348 expect_true(grepl("KorAPConnection", generated_code), "Generated code should include KorAPConnection")
349 expect_true(grepl("corpusQuery", generated_code), "Generated code should include corpusQuery")
350 expect_true(grepl("Hello world", generated_code), "Generated code should include the search term 'Hello world'")
351 expect_true(grepl("fetchAll", generated_code), "Generated code should include fetchAll")
Marc Kupietze759b342025-07-05 19:48:20 +0200352
Marc Kupietz323a4d32026-09-03 12:04:35 +0200353 # Check that the generated code follows the README example pattern
354 expect_true(
355 grepl("\\|>", generated_code) || grepl("%>%", generated_code),
356 "Generated code should use pipe operators"
357 )
Marc Kupietze759b342025-07-05 19:48:20 +0200358
Marc Kupietz323a4d32026-09-03 12:04:35 +0200359 # Test code syntax
360 syntax_valid <- test_code_syntax(generated_code)
361 expect_true(syntax_valid, "Generated code should be syntactically valid R code")
362
363 # Print the generated code for manual inspection
364 cat("Generated corpus query code:\n", generated_code, "\n")
365
366 # Run the code if RUN_LLM_CODE is set
367 execution_result <- run_code_if_enabled(generated_code, "corpus query")
368 if (!is.na(execution_result)) {
369 expect_true(execution_result, "Generated code should execute without runtime errors")
370 }
371 })
Marc Kupietz81ddc342026-09-03 13:18:53 +0200372
373 test_that(paste(model, "can solve corpus size task with README guidance"), {
374 skip_if_offline()
375 skip_if_no_api_key(model)
376 if (llmProvider(model)$name != "synthetic") skip_if_not_installed("tidyllm")
377 skip_if_not(!is.null(find_readme_path()), "Readme.md not found in current or parent directories")
378
379 prompt <- create_readme_prompt(
380 "write R code that reports how many tokens the virtual corpus of newspaper texts published since 2020 contains.",
381 "Write R code to determine the size of a virtual corpus using RKorAPClient."
382 )
383
384 generated_code <- extract_r_code(call_llm_api(prompt, model, max_tokens = 300))
385
386 expect_true(grepl("KorAPConnection", generated_code), "Generated code should include KorAPConnection")
387 expect_true(grepl("corpusStats", generated_code), "Generated code should include corpusStats")
388 expect_true(grepl("vc", generated_code), "Generated code should restrict to a virtual corpus")
389 expect_true(test_code_syntax(generated_code), "Generated code should be syntactically valid R code")
390
391 cat("Generated corpus size code:\n", generated_code, "\n")
392 })
393
394 test_that(paste(model, "can solve text metadata task with README guidance"), {
395 skip_if_offline()
396 skip_if_no_api_key(model)
397 if (llmProvider(model)$name != "synthetic") skip_if_not_installed("tidyllm")
398 skip_if_not(!is.null(find_readme_path()), "Readme.md not found in current or parent directories")
399
400 prompt <- create_readme_prompt(
401 "write R code that retrieves all metadata KorAP holds for the text with the sigle WPD17/L79/98721.",
402 "Write R code to retrieve the metadata of a text using RKorAPClient."
403 )
404
405 generated_code <- extract_r_code(call_llm_api(prompt, model, max_tokens = 300))
406
407 expect_true(grepl("KorAPConnection", generated_code), "Generated code should include KorAPConnection")
408 expect_true(grepl("textMetadata", generated_code), "Generated code should include textMetadata")
409 expect_true(grepl("WPD17/L79/98721", generated_code, fixed = TRUE), "Generated code should include the text sigle")
410 expect_true(test_code_syntax(generated_code), "Generated code should be syntactically valid R code")
411
412 cat("Generated text metadata code:\n", generated_code, "\n")
413 })
414
415 test_that(paste(model, "can solve association score task with README guidance"), {
416 skip_if_offline()
417 skip_if_no_api_key(model)
418 if (llmProvider(model)$name != "synthetic") skip_if_not_installed("tidyllm")
419 skip_if_not(!is.null(find_readme_path()), "Readme.md not found in current or parent directories")
420
421 prompt <- create_readme_prompt(
422 paste(
423 "write R code that computes association scores for the word 'Grund' together with each of the",
424 "collocates 'triftiger' and 'guter', without searching for collocates first."
425 ),
426 "Write R code to compute association scores for known collocation candidates using RKorAPClient."
427 )
428
429 generated_code <- extract_r_code(call_llm_api(prompt, model, max_tokens = 300))
430
431 expect_true(grepl("KorAPConnection", generated_code), "Generated code should include KorAPConnection")
432 expect_true(grepl("collocationScoreQuery", generated_code), "Generated code should include collocationScoreQuery")
433 expect_true(grepl("triftiger", generated_code), "Generated code should include the collocate 'triftiger'")
434 expect_true(grepl("guter", generated_code), "Generated code should include the collocate 'guter'")
435 expect_true(test_code_syntax(generated_code), "Generated code should be syntactically valid R code")
436
437 cat("Generated association score code:\n", generated_code, "\n")
438 })
Marc Kupietz1e9f7912026-09-03 13:23:42 +0200439
440 # The code of the following two tasks cannot reasonably be executed in a test:
441 # authorization needs a browser flow or a token for restricted data, and a
442 # multi-VC collocation analysis runs for minutes. Only the generated code is
443 # inspected, which is the point anyway: can the Readme be followed?
444
445 test_that(paste(model, "can solve authorization task with README guidance"), {
446 skip_if_offline()
447 skip_if_no_api_key(model)
448 if (llmProvider(model)$name != "synthetic") skip_if_not_installed("tidyllm")
449 skip_if_not(!is.null(find_readme_path()), "Readme.md not found in current or parent directories")
450
451 prompt <- create_readme_prompt(
452 paste(
453 "write R code that authorizes the application so that it also receives KWIC snippets from",
454 "corpora with restricted licenses, and then queries 'Ameisenplage' including those snippets."
455 ),
456 "Write R code that authorizes and retrieves KWIC snippets using RKorAPClient."
457 )
458
459 generated_code <- extract_r_code(call_llm_api(prompt, model, max_tokens = 300))
460
461 expect_true(grepl("KorAPConnection", generated_code), "Generated code should include KorAPConnection")
462 expect_true(
463 grepl("auth\\(|accessToken", generated_code),
464 "Generated code should authorize via auth() or an accessToken"
465 )
466 expect_true(
467 grepl("metadataOnly\\s*=\\s*FALSE", generated_code),
468 "Generated code should set metadataOnly = FALSE to receive KWIC snippets"
469 )
470 expect_true(test_code_syntax(generated_code), "Generated code should be syntactically valid R code")
471
472 cat("Generated authorization code:\n", generated_code, "\n")
473 })
474
475 test_that(paste(model, "can solve multi-VC comparison task with README guidance"), {
476 skip_if_offline()
477 skip_if_no_api_key(model)
478 if (llmProvider(model)$name != "synthetic") skip_if_not_installed("tidyllm")
479 skip_if_not(!is.null(find_readme_path()), "Readme.md not found in current or parent directories")
480
481 prompt <- create_readme_prompt(
482 paste(
483 "write R code that compares the collocates of 'Kritik' between newspaper texts published before 2010",
484 "and those published since 2010, and shows those collocates that are attested in both, ordered by how",
485 "differently they are associated."
486 ),
487 "Write R code comparing collocates across two virtual corpora using RKorAPClient."
488 )
489
490 generated_code <- extract_r_code(call_llm_api(prompt, model, max_tokens = 500))
491
492 expect_true(grepl("collocationAnalysis", generated_code), "Generated code should include collocationAnalysis")
493 # the labels of the comparison columns come from the names of the vc vector
494 expect_true(
495 grepl("vc\\s*=\\s*c\\(\\s*[A-Za-z.`\"']", generated_code),
496 "Generated code should pass a named vector of virtual corpora"
497 )
498 # one row per collocate and vc, so the comparison needs to be reduced
499 expect_true(
500 grepl("label", generated_code) || grepl("distinct", generated_code),
501 "Generated code should reduce the result to one row per collocate, via label or distinct()"
502 )
503 # imputed scores describe presence/absence rather than a measured contrast
504 expect_true(grepl("imputed", generated_code), "Generated code should take the imputed flag into account")
505 expect_true(test_code_syntax(generated_code), "Generated code should be syntactically valid R code")
506
507 cat("Generated multi-VC comparison code:\n", generated_code, "\n")
508 })
Marc Kupietz323a4d32026-09-03 12:04:35 +0200509}