Skip to content

Commit 3f6fb8e

Browse files
authored
refactor: improve AI response content handling (#1214)
1 parent bf7aba1 commit 3f6fb8e

File tree

8 files changed

+343
-111
lines changed

8 files changed

+343
-111
lines changed

idea-plugin/src/main/kotlin/com/itangcent/ai/DeepSeekService.kt

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import com.itangcent.http.RawContentType
88
import com.itangcent.http.contentType
99
import com.itangcent.idea.plugin.condition.ConditionOnSetting
1010
import com.itangcent.idea.plugin.settings.helper.AISettingsHelper
11-
import com.itangcent.idea.plugin.utils.AIUtils
1211
import com.itangcent.intellij.extend.sub
1312
import com.itangcent.intellij.logger.Logger
1413
import com.itangcent.suv.http.HttpClientProvider
@@ -87,8 +86,7 @@ open class DeepSeekService : AIService {
8786
val jsonElement = GsonUtils.parseToJsonTree(responseBody)
8887
val content = jsonElement.sub("choices")?.asJsonArray?.firstOrNull()
8988
?.asJsonObject?.sub("message")?.sub("content")?.asString
90-
return content?.let { AIUtils.cleanMarkdownCodeBlocks(it) }
91-
?: throw AIApiException("Could not parse response from DeepSeek API")
89+
return content ?: throw AIApiException("Could not parse response from DeepSeek API")
9290
} catch (e: AIException) {
9391
// Re-throw AI exceptions
9492
throw e

idea-plugin/src/main/kotlin/com/itangcent/ai/LocalLLMClient.kt

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import com.itangcent.common.utils.GsonUtils
44
import com.itangcent.http.HttpClient
55
import com.itangcent.http.RawContentType
66
import com.itangcent.http.contentType
7-
import com.itangcent.idea.plugin.utils.AIUtils
87
import com.itangcent.intellij.extend.sub
98

109
/**
@@ -61,8 +60,7 @@ class LocalLLMClient(
6160
val content = jsonElement.sub("choices")?.asJsonArray?.firstOrNull()
6261
?.asJsonObject?.sub("message")?.sub("content")?.asString
6362
val errMsg = jsonElement.sub("error")?.asString
64-
return content?.let { AIUtils.cleanMarkdownCodeBlocks(it) }
65-
?: throw AIApiException(errMsg ?: "Could not parse response from Local LLM server")
63+
return content ?: throw AIApiException(errMsg ?: "Could not parse response from Local LLM server")
6664
} catch (e: AIException) {
6765
throw e
6866
} catch (e: Exception) {

idea-plugin/src/main/kotlin/com/itangcent/ai/OpenAIService.kt

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import com.itangcent.common.logger.traceError
66
import com.itangcent.idea.plugin.condition.ConditionOnSetting
77
import com.itangcent.idea.plugin.settings.helper.AISettingsHelper
88
import com.itangcent.idea.plugin.settings.helper.HttpSettingsHelper
9-
import com.itangcent.idea.plugin.utils.AIUtils
109
import com.itangcent.intellij.logger.Logger
1110
import com.openai.client.OpenAIClient
1211
import com.openai.client.okhttp.OpenAIOkHttpClient
@@ -64,8 +63,7 @@ open class OpenAIService : AIService {
6463
val content = response.choices().firstOrNull()?.message()?.content()
6564
?.orElse("")
6665

67-
return content?.let { AIUtils.cleanMarkdownCodeBlocks(it) }
68-
?: throw AIApiException("Empty response from OpenAI API")
66+
return content ?: throw AIApiException("Empty response from OpenAI API")
6967
} catch (e: OpenAIException) {
7068
logger.traceError("OpenAI API error: ${e.message}", e)
7169
throw AIApiException("OpenAI API error: ${e.message}", e)

idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/translation/APITranslationHelper.kt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import com.itangcent.common.model.Request
1212
import com.itangcent.common.utils.GsonUtils
1313
import com.itangcent.common.utils.notNullOrEmpty
1414
import com.itangcent.idea.plugin.settings.helper.AISettingsHelper
15+
import com.itangcent.idea.plugin.utils.AIUtils
1516
import com.itangcent.intellij.logger.Logger
1617
import java.util.concurrent.ConcurrentHashMap
1718

@@ -375,10 +376,10 @@ class APITranslationHelper {
375376
"Return only the translated text without any explanations or additional formatting."
376377

377378
// Send translation request to AI
378-
val translatedText = aiService.sendPrompt(systemMessage, text)
379+
val translatedText = AIUtils.getGeneralContent(aiService.sendPrompt(systemMessage, text))
379380

380381
// Cache the result
381-
if (translatedText.notNullOrEmpty()) {
382+
if (translatedText.isNotEmpty()) {
382383
translationCache[cacheKey] = translatedText
383384
return translatedText
384385
}
@@ -434,10 +435,10 @@ class APITranslationHelper {
434435
""".trimIndent()
435436

436437
// Send translation request to AI
437-
val translatedJson = aiService.sendPrompt(systemMessage, jsonString)
438+
val translatedJson = AIUtils.getGeneralContent(aiService.sendPrompt(systemMessage, jsonString))
438439

439440
// Cache the result if it's valid JSON
440-
if (translatedJson.notNullOrEmpty() && isValidJson(translatedJson)) {
441+
if (translatedJson.isNotEmpty() && isValidJson(translatedJson)) {
441442
return translatedJson
442443
}
443444

idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/infer/AIMethodInferHelper.kt

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import com.itangcent.cache.withoutCache
1212
import com.itangcent.common.logger.Log
1313
import com.itangcent.common.logger.traceError
1414
import com.itangcent.common.utils.GsonUtils
15+
import com.itangcent.idea.plugin.utils.AIUtils
1516
import com.itangcent.intellij.context.ActionContext
1617
import com.itangcent.intellij.context.ThreadFlag
1718
import com.itangcent.intellij.extend.isNotActive
@@ -371,14 +372,18 @@ class AIMethodInferHelper : MethodInferHelper {
371372
// Call the AI API with the system message and prompt
372373
val aiResponse = if (currentRetry > 0) {
373374
cacheSwitcher.withoutCache {
374-
aiService.sendPrompt(AIPromptFormatter.METHOD_RETURN_TYPE_INFERENCE_MESSAGE, prompt)
375+
AIUtils.getGeneralContent(
376+
aiService.sendPrompt(AIPromptFormatter.METHOD_RETURN_TYPE_INFERENCE_MESSAGE, prompt)
377+
)
375378
}
376379
} else {
377-
aiService.sendPrompt(AIPromptFormatter.METHOD_RETURN_TYPE_INFERENCE_MESSAGE, prompt)
380+
AIUtils.getGeneralContent(
381+
aiService.sendPrompt(AIPromptFormatter.METHOD_RETURN_TYPE_INFERENCE_MESSAGE, prompt)
382+
)
378383
}
379384

380385
// Check if the response is valid before parsing
381-
if (aiResponse.isBlank()) {
386+
if (aiResponse.isEmpty()) {
382387
logger.warn("Empty AI response received for method ${methodInfo.className}.${methodInfo.methodName}, attempt ${currentRetry + 1}/$maxRetries")
383388
currentRetry++
384389
continue

idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/utils/AIUtils.kt

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,66 @@ object AIUtils {
1313
*/
1414
fun cleanMarkdownCodeBlocks(content: String): String {
1515
val trimmedContent = content.trim()
16-
16+
1717
// Check if content is surrounded by code block delimiters
1818
if (trimmedContent.startsWith("```") && trimmedContent.endsWith("```")) {
1919
// Remove starting delimiter (with optional language identifier)
2020
val withoutStart = trimmedContent.replaceFirst(Regex("^```\\w*", RegexOption.MULTILINE), "")
21-
21+
2222
// Remove ending delimiter - handles both with and without newline
2323
val result = withoutStart.replace(Regex("(\\n)?```$", RegexOption.MULTILINE), "")
24-
24+
2525
// Trim any leading/trailing whitespace
2626
return result.trim()
2727
}
28-
28+
2929
return trimmedContent
3030
}
31+
32+
/**
33+
* Extracts the first code block from content, including the language identifier if present
34+
*
35+
* @param content The content that may contain one or more code blocks
36+
* @return The first code block found, or null if no code block is found
37+
*/
38+
fun extractFirstCodeBlock(content: String): String? {
39+
// First try to find a multi-line code block
40+
val multiLinePattern = Regex("^```\\w*\\n([\\s\\S]*?)```$", RegexOption.MULTILINE)
41+
val multiLineMatch = multiLinePattern.find(content)
42+
if (multiLineMatch != null) {
43+
return multiLineMatch.groupValues[1].trim()
44+
}
45+
46+
// If no multi-line block found, try single-line
47+
val singleLinePattern = Regex("```([^\\n]+?)```", RegexOption.MULTILINE)
48+
val singleLineMatch = singleLinePattern.find(content)
49+
if (singleLineMatch != null) {
50+
return singleLineMatch.groupValues[1].trim()
51+
}
52+
53+
return null
54+
}
55+
56+
/**
57+
* Extracts the first code block of a specific language type from content
58+
*
59+
* @param content The content that may contain one or more code blocks
60+
* @param languageType The specific language type to look for (e.g., "json", "java", "kotlin")
61+
* @return The first code block found with the specified language type, or null if no matching code block is found
62+
*/
63+
fun extractFirstCodeBlockByType(content: String, languageType: String): String? {
64+
val codeBlockPattern = Regex("^```$languageType\\n([\\s\\S]*?)```$", RegexOption.MULTILINE)
65+
return codeBlockPattern.find(content)?.groupValues?.get(1)?.trim()
66+
}
67+
68+
/**
69+
* Gets the general content by first trying to extract a code block.
70+
* If no code block is found, returns the input content.
71+
*
72+
* @param content The content that may contain code blocks
73+
* @return The extracted code block content if found, otherwise the original content
74+
*/
75+
fun getGeneralContent(content: String): String {
76+
return extractFirstCodeBlock(content) ?: content
77+
}
3178
}

0 commit comments

Comments
 (0)