from typing import Optional, Tuple, Union, List, Any
import re
def truncate_output(output: str, max_lines: Optional[int] = 5000) -> str:
"""Truncate output string to keep only the most recent lines if it exceeds max_lines.
When truncation occurs, adds a message indicating how many lines were removed.
Preserves original line endings and handles Unicode characters correctly.
Args:
output: The string output to potentially truncate
max_lines: Maximum number of lines to keep (default: 5000)
Returns:
The truncated string if it exceeded max_lines, or the original string if not
"""
# Handle empty output
if not output:
return ""
# Set max_lines to default if None
if max_lines is None:
max_lines = 5000
# Split while preserving line endings
lines = output.splitlines(keepends=True)
total_lines = len(lines)
# Return original if under limit
if total_lines <= max_lines:
return output
# Calculate lines to remove
lines_removed = total_lines - max_lines
# Keep only the most recent lines
truncated_lines = lines[-max_lines:]
# Add truncation message at start
truncation_msg = f"[{lines_removed} lines of output truncated]\n"
# Combine message with remaining lines
return truncation_msg + "".join(truncated_lines)
def extract_think_tag(text: str) -> Tuple[Optional[str], str]:
"""Extract content from the first ... tag at the start of a string.
Args:
text: Input string that may contain think tags
Returns:
A tuple containing:
- The extracted content from the first think tag (None if no tag found)
- The remaining string after the first think tag (or the original string if no tag found)
"""
# Pattern to match think tags at the start of the string
pattern = r'^\s*(.*?)'
match = re.search(pattern, text, re.DOTALL)
if match:
think_content = match.group(1)
# Get the index where the think tag ends
end_index = match.end()
# Extract the remaining text
remaining_text = text[end_index:]
return think_content, remaining_text
else:
return None, text
def process_thinking_content(
content: Union[str, List[Any]],
supports_think_tag: bool = False,
supports_thinking: bool = False,
panel_title: str = "💠Thoughts",
panel_style: str = None,
show_thoughts: bool = None,
logger = None,
) -> Tuple[Union[str, List[Any]], Optional[str]]:
"""Process model response content to extract and optionally display thinking content.
This function centralizes the logic for extracting and displaying thinking content
from model responses, handling both string content with tags and structured
thinking content (lists).
Args:
content: The model response content (string or list)
supports_think_tag: Whether the model supports tags
supports_thinking: Whether the model supports structured thinking
panel_title: Title to display in the thinking panel
panel_style: Border style for the panel (None uses default)
show_thoughts: Whether to display thinking content (if None, checks config)
logger: Optional logger instance for debug messages
Returns:
A tuple containing:
- The processed content with thinking removed
- The extracted thinking content (None if no thinking found)
"""
extracted_thinking = None
# Skip processing if model doesn't support thinking features
if not (supports_think_tag or supports_thinking):
return content, extracted_thinking
# Determine whether to show thoughts
if show_thoughts is None:
try:
from ra_aid.database.repositories.config_repository import get_config_repository
show_thoughts = get_config_repository().get("show_thoughts", False)
except (ImportError, RuntimeError):
show_thoughts = False
# Handle structured thinking content (list format) from models like Claude 3.7
if isinstance(content, list):
# Extract thinking items and regular content
thinking_items = []
regular_items = []
for item in content:
if isinstance(item, dict) and item.get("type") == "thinking":
thinking_items.append(item.get("text", ""))
else:
regular_items.append(item)
# If we found thinking items, process them
if thinking_items:
extracted_thinking = "\n\n".join(thinking_items)
if logger:
logger.debug(f"Found structured thinking content ({len(extracted_thinking)} chars)")
# Display thinking content if enabled
if show_thoughts:
from rich.panel import Panel
from rich.markdown import Markdown
from rich.console import Console
console = Console()
panel_kwargs = {"title": panel_title}
if panel_style is not None:
panel_kwargs["border_style"] = panel_style
console.print(Panel(Markdown(extracted_thinking), **panel_kwargs))
# Return remaining items as processed content
return regular_items, extracted_thinking
# Handle string content with potential think tags
elif isinstance(content, str):
if logger:
logger.debug("Checking for think tags in response")
think_content, remaining_text = extract_think_tag(content)
if think_content:
extracted_thinking = think_content
if logger:
logger.debug(f"Found think tag content ({len(think_content)} chars)")
# Display thinking content if enabled
if show_thoughts:
from rich.panel import Panel
from rich.markdown import Markdown
from rich.console import Console
console = Console()
panel_kwargs = {"title": panel_title}
if panel_style is not None:
panel_kwargs["border_style"] = panel_style
console.print(Panel(Markdown(think_content), **panel_kwargs))
# Return remaining text as processed content
return remaining_text, extracted_thinking
elif logger:
logger.debug("No think tag content found in response")
# Return the original content if no thinking was found
return content, extracted_thinking