Skip to content

Commit 16fdcfe

Browse files
Fix critical as.logical(from) error and enhance API response handling
- Fixed major bug that occurred when processing large numbers of clusters (60+) - Added comprehensive error handling for strsplit() operations in all API processing functions - Enhanced response validation to prevent function/closure types from being processed as strings - Improved NULL value handling in unlist() operations - Added detailed error logging for better debugging - Updated version to 1.2.4 This resolves the 'cannot coerce type closure to vector of type logical' error that users encountered when processing datasets with many clusters.
1 parent c4d45d5 commit 16fdcfe

13 files changed

+296
-118
lines changed

R/DESCRIPTION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
Package: mLLMCelltype
22
Type: Package
33
Title: Cell Type Annotation Using Large Language Models
4-
Version: 1.2.3
4+
Version: 1.2.4
55
Author: Chen Yang [aut, cre]
66
Maintainer: Chen Yang <[email protected]>
77
Authors@R:

R/NEWS.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,23 @@
11
# mLLMCelltype Changelog
22

3+
## 1.2.4 (2025-05-25)
4+
5+
### Critical Bug Fixes
6+
* **Fixed major `as.logical(from)` error**: Resolved critical error that occurred when processing large numbers of clusters (60+ clusters), which was caused by non-character data being passed to `strsplit()` functions
7+
* **Enhanced error handling for API responses**: Added comprehensive `tryCatch()` blocks around all `strsplit()` operations in API processing functions
8+
* **Improved response validation**: Added robust type checking for API responses to prevent function/closure types from being processed as character strings
9+
10+
### Improvements
11+
* **Enhanced API processing robustness**: All API processing functions (`process_openrouter.R`, `process_anthropic.R`, `process_openai.R`, `process_deepseek.R`, `process_qwen.R`, `process_stepfun.R`, `process_minimax.R`, `process_zhipu.R`, `process_gemini.R`, `process_grok.R`) now include improved error handling
12+
* **Better NULL value handling**: Improved `unlist()` operations to filter out NULL values and handle errors gracefully
13+
* **Enhanced logging**: Added more detailed error logging for debugging API response issues
14+
* **Improved consensus checking**: Enhanced `check_consensus.R` to handle edge cases with malformed responses
15+
16+
### Technical Details
17+
* Fixed issue where large cluster datasets could cause type coercion errors in response parsing
18+
* Added validation for function/closure types in API responses to prevent downstream errors
19+
* Improved error messages to provide better diagnostics for API response issues
20+
321
## 1.2.3 (2025-05-10)
422

523
### Bug Fixes

R/R/check_consensus.R

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -136,19 +136,42 @@ check_consensus <- function(round_responses, api_keys = NULL, controversy_thresh
136136

137137
# Directly parse the response using a simpler approach
138138
# First, check if response is NULL or empty
139-
if (is.null(response) || length(response) == 0 || nchar(response) == 0) {
140-
write_log("WARNING: Response is NULL, empty, or has zero length")
139+
if (is.null(response) || length(response) == 0) {
140+
write_log("WARNING: Response is NULL or has zero length")
141141
lines <- c("0", "0", "0", "Unknown")
142142
} else if (!is.character(response)) {
143143
# If response is not a character, convert it to string
144144
write_log(sprintf("WARNING: Response is not a character but %s, converting to string", typeof(response)))
145-
response <- as.character(response)
145+
# Handle different types more carefully
146+
if (is.function(response)) {
147+
write_log("ERROR: Response is a function (closure), this indicates a serious error in the API response processing")
148+
lines <- c("0", "0", "0", "Unknown")
149+
} else {
150+
tryCatch({
151+
response <- as.character(response)
152+
if (nchar(response) == 0) {
153+
lines <- c("0", "0", "0", "Unknown")
154+
} else {
155+
lines <- c(response)
156+
}
157+
}, error = function(e) {
158+
write_log(sprintf("ERROR: Failed to convert response to character: %s", e$message))
159+
lines <- c("0", "0", "0", "Unknown")
160+
})
161+
}
162+
} else if (nchar(response) == 0) {
163+
write_log("WARNING: Response is empty string")
146164
lines <- c("0", "0", "0", "Unknown")
147165
} else if (grepl("\n", response)) {
148166
# Split by newlines and clean up
149-
lines <- strsplit(response, "\n")[[1]]
150-
lines <- trimws(lines)
151-
lines <- lines[nchar(lines) > 0]
167+
tryCatch({
168+
lines <- strsplit(response, "\n")[[1]]
169+
lines <- trimws(lines)
170+
lines <- lines[nchar(lines) > 0]
171+
}, error = function(e) {
172+
write_log(sprintf("ERROR: Failed to split response by newlines: %s", e$message))
173+
lines <- c("0", "0", "0", "Unknown")
174+
})
152175
} else {
153176
# If no newlines, treat as a single line
154177
lines <- c(response)

R/R/process_anthropic.R

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -83,16 +83,28 @@ process_anthropic <- function(prompt, model, api_key) {
8383
write_log("ERROR: Response content is not a character string")
8484
write_log(sprintf("Response content type: %s", typeof(response_content)))
8585
write_log(sprintf("Response content structure: %s", jsonlite::toJSON(content$content[[1]], auto_unbox = TRUE, pretty = TRUE)))
86-
return(NULL)
86+
return(c("Error: Invalid response format"))
8787
}
8888

89-
res <- strsplit(response_content, '\n')[[1]]
90-
write_log(sprintf("Got response with %d lines", length(res)))
91-
write_log(sprintf("Raw response from Claude:\n%s", paste(res, collapse = "\n")))
92-
93-
res
89+
tryCatch({
90+
res <- strsplit(response_content, '\n')[[1]]
91+
write_log(sprintf("Got response with %d lines", length(res)))
92+
write_log(sprintf("Raw response from Claude:\n%s", paste(res, collapse = "\n")))
93+
res
94+
}, error = function(e) {
95+
write_log(sprintf("ERROR: Failed to split response content: %s", e$message))
96+
return(c("Error: Failed to parse response"))
97+
})
9498
}, simplify = FALSE)
9599

96100
write_log("All chunks processed successfully")
97-
return(gsub(',$', '', unlist(allres)))
101+
102+
# Filter out NULL values and handle errors more gracefully
103+
valid_results <- allres[!sapply(allres, is.null)]
104+
if (length(valid_results) == 0) {
105+
write_log("ERROR: No valid responses received from Anthropic")
106+
return(c("Error: No valid responses"))
107+
}
108+
109+
return(gsub(',$', '', unlist(valid_results)))
98110
}

R/R/process_deepseek.R

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -80,17 +80,29 @@ process_deepseek <- function(prompt, model, api_key) {
8080
write_log("ERROR: Response content is not a character string")
8181
write_log(sprintf("Response content type: %s", typeof(response_content)))
8282
write_log(sprintf("Response content structure: %s", jsonlite::toJSON(content$choices[[1]]$message, auto_unbox = TRUE, pretty = TRUE)))
83-
return(NULL)
83+
return(c("Error: Invalid response format"))
8484
}
8585

8686
# DeepSeek's response is in content$choices[[1]]$message$content
87-
res <- strsplit(response_content, '\n')[[1]]
88-
write_log(sprintf("Got response with %d lines", length(res)))
89-
write_log(sprintf("Raw response from DeepSeek:\n%s", paste(res, collapse = "\n")))
90-
91-
res
87+
tryCatch({
88+
res <- strsplit(response_content, '\n')[[1]]
89+
write_log(sprintf("Got response with %d lines", length(res)))
90+
write_log(sprintf("Raw response from DeepSeek:\n%s", paste(res, collapse = "\n")))
91+
res
92+
}, error = function(e) {
93+
write_log(sprintf("ERROR: Failed to split response content: %s", e$message))
94+
return(c("Error: Failed to parse response"))
95+
})
9296
}, simplify = FALSE)
9397

9498
write_log("All chunks processed successfully")
95-
return(gsub(',$', '', unlist(allres)))
99+
100+
# Filter out NULL values and handle errors more gracefully
101+
valid_results <- allres[!sapply(allres, is.null)]
102+
if (length(valid_results) == 0) {
103+
write_log("ERROR: No valid responses received from DeepSeek")
104+
return(c("Error: No valid responses"))
105+
}
106+
107+
return(gsub(',$', '', unlist(valid_results)))
96108
}

R/R/process_gemini.R

Lines changed: 56 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,36 +3,36 @@
33
process_gemini <- function(prompt, model, api_key) {
44
write_log("\n=== Starting Gemini API Request ===\n")
55
write_log(sprintf("Model: %s", model))
6-
6+
77
# Gemini API endpoint
88
base_url <- "https://generativelanguage.googleapis.com/v1beta/models"
99
url <- sprintf("%s/%s:generateContent?key=%s", base_url, model, api_key)
1010
write_log("API URL:")
1111
write_log(url)
12-
12+
1313
# Process all input at once
1414
input_lines <- strsplit(prompt, "\n")[[1]]
1515
write_log("\nInput lines:")
1616
write_log(paste(input_lines, collapse = "\n"))
17-
17+
1818
cutnum <- 1 # Changed to always use 1 chunk
1919
write_log(sprintf("\nProcessing input in %d chunk(s)", cutnum))
20-
20+
2121
if (cutnum > 1) {
22-
cid <- as.numeric(cut(1:length(input_lines), cutnum))
22+
cid <- as.numeric(cut(1:length(input_lines), cutnum))
2323
} else {
2424
cid <- rep(1, length(input_lines))
2525
}
26-
26+
2727
# Process each chunk
2828
allres <- sapply(1:cutnum, function(i) {
2929
write_log(sprintf("\nProcessing chunk %d of %d", i, cutnum))
3030
id <- which(cid == i)
31-
31+
3232
chunk_content <- paste(input_lines[id], collapse = '\n')
3333
write_log("\nChunk content:")
3434
write_log(chunk_content)
35-
35+
3636
# Prepare the request body
3737
body <- list(
3838
contents = list(
@@ -45,10 +45,10 @@ process_gemini <- function(prompt, model, api_key) {
4545
)
4646
)
4747
)
48-
48+
4949
write_log("\nRequest body:")
5050
write_log(jsonlite::toJSON(body, auto_unbox = TRUE, pretty = TRUE))
51-
51+
5252
write_log("\nSending API request...")
5353
# Make the API request
5454
response <- httr::POST(
@@ -59,19 +59,19 @@ process_gemini <- function(prompt, model, api_key) {
5959
body = jsonlite::toJSON(body, auto_unbox = TRUE),
6060
encode = "json"
6161
)
62-
62+
6363
# Check for errors
6464
if (httr::http_error(response)) {
6565
error_message <- httr::content(response, "parsed")
66-
write_log(sprintf("ERROR: Gemini API request failed: %s",
66+
write_log(sprintf("ERROR: Gemini API request failed: %s",
6767
if (!is.null(error_message$error$message)) error_message$error$message else "Unknown error"))
6868
return(NULL)
6969
}
70-
70+
7171
write_log("Parsing API response...")
7272
# Parse the response
7373
content <- httr::content(response, "parsed")
74-
74+
7575
# Add robust error handling for response structure
7676
tryCatch({
7777
# Check if the response has the expected structure
@@ -80,23 +80,47 @@ process_gemini <- function(prompt, model, api_key) {
8080
write_log(sprintf("Response content: %s", jsonlite::toJSON(content, auto_unbox = TRUE, pretty = TRUE)))
8181
return("Error: Unexpected API response structure")
8282
}
83-
83+
8484
candidate <- content$candidates[[1]]
85-
85+
8686
# For Gemini 1.0 models
8787
if (!is.null(candidate$content$parts[[1]]$text)) {
88-
res <- strsplit(candidate$content$parts[[1]]$text, '\n')[[1]]
89-
}
88+
text_content <- candidate$content$parts[[1]]$text
89+
if (is.character(text_content)) {
90+
res <- strsplit(text_content, '\n')[[1]]
91+
} else {
92+
write_log("ERROR: Text content is not a character string")
93+
return("Error: Invalid text content format")
94+
}
95+
}
9096
# For Gemini 1.5/2.5 models (may have different structure)
9197
else if (!is.null(candidate$text)) {
92-
res <- strsplit(candidate$text, '\n')[[1]]
98+
text_content <- candidate$text
99+
if (is.character(text_content)) {
100+
res <- strsplit(text_content, '\n')[[1]]
101+
} else {
102+
write_log("ERROR: Text content is not a character string")
103+
return("Error: Invalid text content format")
104+
}
93105
}
94106
# Try other possible response structures
95107
else if (!is.null(candidate$content$text)) {
96-
res <- strsplit(candidate$content$text, '\n')[[1]]
108+
text_content <- candidate$content$text
109+
if (is.character(text_content)) {
110+
res <- strsplit(text_content, '\n')[[1]]
111+
} else {
112+
write_log("ERROR: Text content is not a character string")
113+
return("Error: Invalid text content format")
114+
}
97115
}
98116
else if (!is.null(content$text)) {
99-
res <- strsplit(content$text, '\n')[[1]]
117+
text_content <- content$text
118+
if (is.character(text_content)) {
119+
res <- strsplit(text_content, '\n')[[1]]
120+
} else {
121+
write_log("ERROR: Text content is not a character string")
122+
return("Error: Invalid text content format")
123+
}
100124
}
101125
else {
102126
# If we can't find the text in any expected location, log the structure and return an error
@@ -111,10 +135,18 @@ process_gemini <- function(prompt, model, api_key) {
111135
})
112136
write_log(sprintf("Got response with %d lines", length(res)))
113137
write_log(sprintf("Raw response from Gemini:\n%s", paste(res, collapse = "\n")))
114-
138+
115139
res
116140
}, simplify = FALSE)
117-
141+
118142
write_log("All chunks processed successfully")
119-
return(gsub(',$', '', unlist(allres)))
143+
144+
# Filter out NULL values and handle errors more gracefully
145+
valid_results <- allres[!sapply(allres, is.null)]
146+
if (length(valid_results) == 0) {
147+
write_log("ERROR: No valid responses received from Gemini")
148+
return(c("Error: No valid responses"))
149+
}
150+
151+
return(gsub(',$', '', unlist(valid_results)))
120152
}

R/R/process_grok.R

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -109,21 +109,28 @@ process_grok <- function(prompt, model, api_key) {
109109
write_log("ERROR: Response content is not a character string")
110110
write_log(sprintf("Response content type: %s", typeof(response_content)))
111111
write_log(sprintf("Response content structure: %s", jsonlite::toJSON(content$choices[[1]]$message, auto_unbox = TRUE, pretty = TRUE)))
112-
return(NULL)
113-
}
114-
115-
res <- strsplit(response_content, '\n')[[1]]
116-
117-
# Log usage information if available
118-
if (!is.null(content$usage)) {
119-
write_log(sprintf("Tokens used - Prompt: %d, Completion: %d, Total: %d",
120-
content$usage$prompt_tokens,
121-
content$usage$completion_tokens,
122-
content$usage$total_tokens))
112+
return(c("Error: Invalid response format"))
123113
}
124114

125-
write_log(sprintf("Got response with %d lines", length(res)))
126-
write_log(sprintf("Raw response from Grok:\n%s", paste(res, collapse = "\n")))
115+
res <- tryCatch({
116+
split_result <- strsplit(response_content, '\n')[[1]]
117+
118+
# Log usage information if available
119+
if (!is.null(content$usage)) {
120+
write_log(sprintf("Tokens used - Prompt: %d, Completion: %d, Total: %d",
121+
content$usage$prompt_tokens,
122+
content$usage$completion_tokens,
123+
content$usage$total_tokens))
124+
}
125+
126+
write_log(sprintf("Got response with %d lines", length(split_result)))
127+
write_log(sprintf("Raw response from Grok:\n%s", paste(split_result, collapse = "\n")))
128+
129+
split_result
130+
}, error = function(e) {
131+
write_log(sprintf("ERROR: Failed to split response content: %s", e$message))
132+
return(c("Error: Failed to parse response"))
133+
})
127134

128135
res
129136
}, simplify = FALSE)

0 commit comments

Comments
 (0)