diff --git a/swarms/utils/formatter.py b/swarms/utils/formatter.py index 0fb3f4f0..8fbae704 100644 --- a/swarms/utils/formatter.py +++ b/swarms/utils/formatter.py @@ -1,5 +1,6 @@ import threading import time +import re from typing import Any, Callable, Dict, List, Optional from rich.console import Console @@ -13,6 +14,8 @@ from rich.progress import ( from rich.table import Table from rich.text import Text from rich.spinner import Spinner +from rich.markdown import Markdown +from rich.syntax import Syntax # Global lock to ensure only a single Rich Live context is active at any moment. # Rich's Live render is **not** thread-safe; concurrent Live contexts on the same @@ -28,6 +31,136 @@ dashboard_live = None spinner = Spinner("dots", style="yellow") +class MarkdownOutputHandler: + """Custom output handler to render content as markdown with simplified syntax highlighting""" + + def __init__(self, console: Console): + self.console = console + + def _clean_output(self, output: str) -> str: + """Clean up the output for better markdown rendering""" + if not output: + return "" + + # Remove log prefixes and timestamps + output = re.sub(r'\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} \| INFO.*?\|.*?\|', '', output) + output = re.sub(r'\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} \| DEBUG.*?\|.*?\|', '', output) + output = re.sub(r'\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} \| WARNING.*?\|.*?\|', '', output) + output = re.sub(r'\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} \| ERROR.*?\|.*?\|', '', output) + + # Remove spinner characters and progress indicators + output = re.sub(r'[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏]', '', output) + output = re.sub(r'⠋ Processing\.\.\.', '', output) + output = re.sub(r'⠙ Processing\.\.\.', '', output) + output = re.sub(r'⠹ Processing\.\.\.', '', output) + output = re.sub(r'⠸ Processing\.\.\.', '', output) + output = re.sub(r'⠼ Processing\.\.\.', '', output) + output = re.sub(r'⠴ Processing\.\.\.', '', output) + output = re.sub(r'⠦ Processing\.\.\.', '', output) + output = re.sub(r'⠧ Processing\.\.\.', '', output) + output = re.sub(r'⠇ Processing\.\.\.', '', output) + output = re.sub(r'⠏ Processing\.\.\.', '', output) + + # Remove loop indicators + output = re.sub(r'⠋ Loop \d+/\d+', '', output) + output = re.sub(r'⠙ Loop \d+/\d+', '', output) + output = re.sub(r'⠹ Loop \d+/\d+', '', output) + output = re.sub(r'⠸ Loop \d+/\d+', '', output) + output = re.sub(r'⠼ Loop \d+/\d+', '', output) + output = re.sub(r'⠴ Loop \d+/\d+', '', output) + output = re.sub(r'⠦ Loop \d+/\d+', '', output) + output = re.sub(r'⠧ Loop \d+/\d+', '', output) + output = re.sub(r'⠇ Loop \d+/\d+', '', output) + output = re.sub(r'⠏ Loop \d+/\d+', '', output) + + # Remove any remaining log messages + output = re.sub(r'INFO.*?\|.*?\|.*?\|', '', output) + output = re.sub(r'DEBUG.*?\|.*?\|.*?\|', '', output) + output = re.sub(r'WARNING.*?\|.*?\|.*?\|', '', output) + output = re.sub(r'ERROR.*?\|.*?\|.*?\|', '', output) + + # Clean up extra whitespace and empty lines + output = re.sub(r'\n\s*\n\s*\n', '\n\n', output) + output = re.sub(r'^\s+', '', output, flags=re.MULTILINE) + output = re.sub(r'\s+$', '', output, flags=re.MULTILINE) + + # Remove any remaining plaintext artifacts + output = re.sub(r'Generated content:', '', output) + output = re.sub(r'Evaluation result:', '', output) + output = re.sub(r'Refined content:', '', output) + + # Ensure proper markdown formatting + if not output.strip().startswith('#'): + # If no headers, add some structure + lines = output.strip().split('\n') + if len(lines) > 0: + # Add a header for the first meaningful line + first_line = lines[0].strip() + if first_line and not first_line.startswith('**'): + output = f"## {first_line}\n\n" + '\n'.join(lines[1:]) + + return output.strip() + + def render_with_simple_syntax_highlighting(self, content: str) -> list: + """Render content with simplified syntax highlighting for code blocks""" + # For now, let's just render everything as markdown to ensure it works + # We can add code block detection back later if needed + return [('markdown', content)] + + def render_content_parts(self, parts: list) -> list: + """Render different content parts with appropriate formatting""" + rendered_parts = [] + + for part in parts: + if part[0] == 'markdown': + # Render markdown + try: + md = Markdown(part[1]) + rendered_parts.append(md) + except Exception: + # Fallback to plain text + rendered_parts.append(Text(part[1])) + + elif part[0] == 'code': + # Code is already rendered as Syntax or Text object + rendered_parts.append(part[1]) + + return rendered_parts + + def render_markdown_output(self, content: str, title: str = "", border_style: str = "blue"): + """Render content as markdown with syntax highlighting""" + if not content or content.strip() == "": + return + + # Clean up the output + cleaned_content = self._clean_output(content) + + # Render with syntax highlighting + try: + # Split content into parts (markdown and code blocks) + parts = self.render_with_simple_syntax_highlighting(cleaned_content) + + # Render each part appropriately + rendered_parts = self.render_content_parts(parts) + + # Create a group of rendered parts + from rich.console import Group + content_group = Group(*rendered_parts) + + self.console.print(Panel( + content_group, + title=title, + border_style=border_style + )) + except Exception as e: + # Fallback to plain text if rendering fails + self.console.print(Panel( + cleaned_content, + title=title, + border_style="yellow" + )) + + def choose_random_color(): import random @@ -50,9 +183,12 @@ class Formatter: A class for formatting and printing rich text to the console. """ - def __init__(self): + def __init__(self, markdown: bool = True): """ Initializes the Formatter with a Rich Console instance. + + Args: + markdown (bool): Whether to enable markdown output rendering. Defaults to True. """ self.console = Console() self._dashboard_live = None @@ -69,6 +205,29 @@ class Formatter: "⠏", ] self._spinner_idx = 0 + self.markdown = markdown + + # Initialize markdown output handler if enabled + if self.markdown: + self.markdown_handler = MarkdownOutputHandler(self.console) + else: + self.markdown_handler = None + + def enable_markdown(self): + """Enable markdown output rendering""" + if not self.markdown: + self.markdown = True + self.markdown_handler = MarkdownOutputHandler(self.console) + + def disable_markdown(self): + """Disable markdown output rendering""" + if self.markdown: + self.markdown = False + self.markdown_handler = None + + def is_markdown_enabled(self) -> bool: + """Check if markdown output is enabled""" + return self.markdown def _get_status_with_loading(self, status: str) -> Text: """ @@ -142,12 +301,36 @@ class Formatter: if not isinstance(content, str): content = str(content) - try: - self._print_panel(content, title, style) - except Exception: - # Fallback to basic printing if panel fails - print(f"\n{title}:") - print(content) + # Use markdown rendering if enabled + if self.markdown and self.markdown_handler: + self.markdown_handler.render_markdown_output(content, title, style) + else: + # Fallback to original panel printing + try: + self._print_panel(content, title, style) + except Exception: + # Fallback to basic printing if panel fails + print(f"\n{title}:") + print(content) + + def print_markdown( + self, + content: str, + title: str = "", + border_style: str = "blue", + ) -> None: + """Print content as markdown with syntax highlighting. + + Args: + content (str): The content to display as markdown + title (str): The title of the panel + border_style (str): The border style for the panel + """ + if self.markdown_handler: + self.markdown_handler.render_markdown_output(content, title, border_style) + else: + # Fallback to regular panel if markdown is disabled + self.print_panel(content, title, border_style) def print_table( self, title: str, data: Dict[str, List[str]] @@ -397,7 +580,7 @@ class Formatter: def print_agent_dashboard( self, agents_data: List[Dict[str, Any]], - title: str = "ConcurrentWorkflow Dashboard", + title: str = "Concurrent Workflow Dashboard", is_final: bool = False, ) -> None: """ @@ -440,4 +623,5 @@ class Formatter: self._dashboard_live = None -formatter = Formatter() +# Global formatter instance with markdown output enabled by default +formatter = Formatter(markdown=True)