package cc.unitmesh.agent.render /** * Base abstract renderer providing common functionality * All specific renderer implementations should extend this class */ abstract class BaseRenderer : CodingAgentRenderer { protected val reasoningBuffer = StringBuilder() protected var isInDevinBlock = false protected var isInThinkBlock = false protected var lastIterationReasoning = "" protected var consecutiveRepeats = 0 protected val thinkingBuffer = StringBuilder() /** * Common devin block filtering logic * Note: This does NOT filter blocks - those are handled separately */ protected fun filterDevinBlocks(content: String): String { var filtered = content // Remove complete devin blocks filtered = filtered.replace(Regex("]*>[\\s\\S]*?"), "") // Handle incomplete devin blocks at the end val openDevinIndex = filtered.lastIndexOf("", openDevinIndex) if (closeDevinIndex == -1) { // Incomplete devin block, remove it filtered = filtered.substring(0, openDevinIndex) } } // Remove partial devin tags // IMPORTANT: // We only want to strip partial "") // Check for partial opening tags // Only treat partial " lastCloseDevin || hasPartialTag } /** * Process content and extract thinking blocks. * Returns a pair of (contentWithoutThinking, thinkingContent) * * Handles both complete and streaming ... blocks. */ protected fun extractThinkingContent(content: String): ThinkingExtractionResult { val result = ThinkingExtractionResult() var remaining = content // Handle complete ... blocks val completeThinkPattern = Regex("([\\s\\S]*?)") val matches = completeThinkPattern.findAll(remaining) for (match in matches) { result.thinkingContent.append(match.groupValues[1]) result.hasCompleteThinkBlock = true } remaining = remaining.replace(completeThinkPattern, "") // Check for incomplete block at the end val openThinkIndex = remaining.lastIndexOf("") if (openThinkIndex != -1) { val closeThinkIndex = remaining.indexOf("", openThinkIndex) if (closeThinkIndex == -1) { // Incomplete think block - extract content after val thinkContent = remaining.substring(openThinkIndex + 7) result.thinkingContent.append(thinkContent) result.hasIncompleteThinkBlock = true remaining = remaining.substring(0, openThinkIndex) } } // Check for partial 0) commonWords.size.toDouble() / totalWords else 0.0 } /** * Clean up excessive newlines in content */ protected fun cleanNewlines(content: String): String { return content.replace(Regex("\n{3,}"), "\n\n") } override fun renderLLMResponseStart() { reasoningBuffer.clear() isInDevinBlock = false } /** * Default implementation for LLM response end with similarity checking */ override fun renderLLMResponseEnd() { val currentReasoning = reasoningBuffer.toString().trim() val similarity = calculateSimilarity(currentReasoning, lastIterationReasoning) if (similarity > 0.95 && lastIterationReasoning.isNotEmpty()) { consecutiveRepeats++ if (consecutiveRepeats >= 2) { renderRepeatAnalysisWarning() } } else { consecutiveRepeats = 0 } lastIterationReasoning = currentReasoning } /** * Render warning for repetitive analysis - can be overridden by subclasses */ protected open fun renderRepeatAnalysisWarning() { renderError("Agent appears to be repeating similar analysis...") } }