From 169d3233e826ae971ee591f95573094812088ec5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moura?= Date: Wed, 21 May 2025 09:44:56 -0700 Subject: [PATCH] Updating Logging (#2874) --- docs/concepts/reasoning.mdx | 2 +- src/crewai/telemetry/telemetry.py | 2 +- .../events/utils/console_formatter.py | 223 ++++++++++++------ 3 files changed, 147 insertions(+), 80 deletions(-) diff --git a/docs/concepts/reasoning.mdx b/docs/concepts/reasoning.mdx index 0f7644d9a..c6890f71c 100644 --- a/docs/concepts/reasoning.mdx +++ b/docs/concepts/reasoning.mdx @@ -116,7 +116,7 @@ Here's an example of what a reasoning plan might look like for a data analysis t Task: Analyze the provided sales data and identify key trends. Reasoning Plan: -I'll analyze the sales data to identify the top 3 trends. +I'll analyze the sales data to identify the top 3 trends. 1. Understanding of the task: I need to analyze sales data to identify key trends that would be valuable for business decision-making. diff --git a/src/crewai/telemetry/telemetry.py b/src/crewai/telemetry/telemetry.py index e22d757cd..59d613a55 100644 --- a/src/crewai/telemetry/telemetry.py +++ b/src/crewai/telemetry/telemetry.py @@ -64,7 +64,7 @@ class Telemetry: attribute in the Crew class. """ - def __init__(self): + def __init__(self) -> None: self.ready: bool = False self.trace_set: bool = False diff --git a/src/crewai/utilities/events/utils/console_formatter.py b/src/crewai/utilities/events/utils/console_formatter.py index e3ae471d2..72a02b2f7 100644 --- a/src/crewai/utilities/events/utils/console_formatter.py +++ b/src/crewai/utilities/events/utils/console_formatter.py @@ -4,6 +4,7 @@ from rich.console import Console from rich.panel import Panel from rich.text import Text from rich.tree import Tree +from rich.live import Live class ConsoleFormatter: @@ -20,6 +21,12 @@ class ConsoleFormatter: def __init__(self, verbose: bool = False): self.console = Console(width=None) self.verbose = verbose + # Live instance to dynamically update a Tree renderable (e.g. the Crew tree) + # When multiple Tree objects are printed sequentially we reuse this Live + # instance so the previous render is replaced instead of writing a new one. + # Once any non-Tree renderable is printed we stop the Live session so the + # final Tree persists on the terminal. + self._live: Optional[Live] = None def create_panel(self, content: Text, title: str, style: str = "blue") -> Panel: """Create a standardized panel with consistent styling.""" @@ -60,7 +67,7 @@ class ConsoleFormatter: label.append(f"{prefix} ", style=f"{style} bold") label.append(name, style=style) if status: - label.append("\n Status: ", style="white") + label.append("\nStatus: ", style="white") label.append(status, style=f"{style} bold") tree.label = label @@ -69,7 +76,46 @@ class ConsoleFormatter: return parent.add(Text(text, style=style)) def print(self, *args, **kwargs) -> None: - """Print to console with consistent formatting if verbose is enabled.""" + """Custom print that replaces consecutive Tree renders. + + * If the argument is a single ``Tree`` instance, we either start a + ``Live`` session (first tree) or update the existing one (subsequent + trees). This results in the tree being rendered in-place instead of + being appended repeatedly to the log. + + * A blank call (no positional arguments) is ignored while a Live + session is active so it does not prematurely terminate the tree + rendering. + + * Any other renderable will terminate the Live session (if one is + active) so the last tree stays on screen and the new content is + printed normally. + """ + + # Case 1: updating / starting live Tree rendering + if len(args) == 1 and isinstance(args[0], Tree): + tree = args[0] + + if not self._live: + # Start a new Live session for the first tree + self._live = Live(tree, console=self.console, refresh_per_second=4) + self._live.start() + else: + # Update existing Live session + self._live.update(tree, refresh=True) + return # Nothing else to do + + # Case 2: blank line while a live session is running โ€“ ignore so we + # don't break the in-place rendering behaviour + if len(args) == 0 and self._live: + return + + # Case 3: printing something other than a Tree โ†’ terminate live session + if self._live: + self._live.stop() + self._live = None + + # Finally, pass through to the regular Console.print implementation self.console.print(*args, **kwargs) def print_panel( @@ -157,7 +203,7 @@ class ConsoleFormatter: task_content = Text() task_content.append(f"๐Ÿ“‹ Task: {task_id}", style="yellow bold") - task_content.append("\n Status: ", style="white") + task_content.append("\nStatus: ", style="white") task_content.append("Executing Task...", style="yellow dim") task_branch = None @@ -197,11 +243,17 @@ class ConsoleFormatter: # Update tree label for branch in crew_tree.children: if str(task_id) in str(branch.label): + # Build label without introducing stray blank lines task_content = Text() + # First line: Task ID task_content.append(f"๐Ÿ“‹ Task: {task_id}", style=f"{style} bold") - task_content.append("\n Assigned to: ", style="white") + + # Second line: Assigned to + task_content.append("\nAssigned to: ", style="white") task_content.append(agent_role, style=style) - task_content.append("\n Status: ", style="white") + + # Third line: Status + task_content.append("Status: ", style="white") task_content.append(status_text, style=f"{style} bold") branch.label = task_content self.print(crew_tree) @@ -220,18 +272,16 @@ class ConsoleFormatter: if not self.verbose or not task_branch or not crew_tree: return None - agent_branch = task_branch.add("") - self.update_tree_label( - agent_branch, "๐Ÿค– Agent:", agent_role, "green", "In Progress" - ) + # Instead of creating a separate Agent node, we treat the task branch + # itself as the logical agent branch so that Reasoning/Tool nodes are + # nested under the task without an extra visual level. - self.print(crew_tree) - self.print() + # Store the task branch as the current_agent_branch for future nesting. + self.current_agent_branch = task_branch - # Set the current_agent_branch attribute directly - self.current_agent_branch = agent_branch - - return agent_branch + # No additional tree modification needed; return the task branch so + # caller logic remains unchanged. + return task_branch def update_agent_status( self, @@ -241,19 +291,10 @@ class ConsoleFormatter: status: str = "completed", ) -> None: """Update agent status in the tree.""" - if not self.verbose or agent_branch is None or crew_tree is None: - return - - self.update_tree_label( - agent_branch, - "๐Ÿค– Agent:", - agent_role, - "green", - "โœ… Completed" if status == "completed" else "โŒ Failed", - ) - - self.print(crew_tree) - self.print() + # We no longer render a separate agent branch, so this method simply + # updates the stored branch reference (already the task branch) without + # altering the tree. Keeping it a no-op avoids duplicate status lines. + return def create_flow_tree(self, flow_name: str, flow_id: str) -> Optional[Tree]: """Create and initialize a flow tree.""" @@ -266,7 +307,7 @@ class ConsoleFormatter: flow_label = Text() flow_label.append("๐ŸŒŠ Flow: ", style="blue bold") flow_label.append(flow_name, style="blue") - flow_label.append("\n ID: ", style="white") + flow_label.append("\nID: ", style="white") flow_label.append(flow_id, style="blue") flow_tree = Tree(flow_label) @@ -281,7 +322,7 @@ class ConsoleFormatter: flow_label = Text() flow_label.append("๐ŸŒŠ Flow: ", style="blue bold") flow_label.append(flow_name, style="blue") - flow_label.append("\n ID: ", style="white") + flow_label.append("\nID: ", style="white") flow_label.append(flow_id, style="blue") flow_tree.label = flow_label @@ -395,9 +436,15 @@ class ConsoleFormatter: if not self.verbose: return None - # Use LiteAgent branch if available, otherwise use regular agent branch - branch_to_use = self.current_lite_agent_branch or agent_branch - tree_to_use = branch_to_use or crew_tree + # Parent for tool usage: LiteAgent > Agent > Task + branch_to_use = ( + self.current_lite_agent_branch + or agent_branch + or self.current_task_branch + ) + + # Render full crew tree when available for consistent live updates + tree_to_use = self.current_crew_tree or crew_tree or branch_to_use if branch_to_use is None or tree_to_use is None: # If we don't have a valid branch, default to crew_tree if provided @@ -423,10 +470,9 @@ class ConsoleFormatter: "yellow", ) - # Only print if this is a new tool usage - if tool_branch not in branch_to_use.children: - self.print(tree_to_use) - self.print() + # Print updated tree immediately + self.print(tree_to_use) + self.print() return tool_branch @@ -440,8 +486,8 @@ class ConsoleFormatter: if not self.verbose or tool_branch is None: return - # Use LiteAgent branch if available, otherwise use crew tree - tree_to_use = self.current_lite_agent_branch or crew_tree + # Decide which tree to render: prefer full crew tree, else parent branch + tree_to_use = self.current_crew_tree or crew_tree or self.current_task_branch if tree_to_use is None: return @@ -472,8 +518,8 @@ class ConsoleFormatter: if not self.verbose: return - # Use LiteAgent branch if available, otherwise use crew tree - tree_to_use = self.current_lite_agent_branch or crew_tree + # Decide which tree to render: prefer full crew tree, else parent branch + tree_to_use = self.current_crew_tree or crew_tree or self.current_task_branch if tool_branch: self.update_tree_label( @@ -501,9 +547,15 @@ class ConsoleFormatter: if not self.verbose: return None - # Use LiteAgent branch if available, otherwise use regular agent branch - branch_to_use = self.current_lite_agent_branch or agent_branch - tree_to_use = branch_to_use or crew_tree + # Parent for tool usage: LiteAgent > Agent > Task + branch_to_use = ( + self.current_lite_agent_branch + or agent_branch + or self.current_task_branch + ) + + # Render full crew tree when available for consistent live updates + tree_to_use = self.current_crew_tree or crew_tree or branch_to_use if branch_to_use is None or tree_to_use is None: # If we don't have a valid branch, default to crew_tree if provided @@ -532,24 +584,33 @@ class ConsoleFormatter: if not self.verbose or tool_branch is None: return - # Use LiteAgent branch if available, otherwise use regular agent branch - branch_to_use = self.current_lite_agent_branch or agent_branch - tree_to_use = branch_to_use or crew_tree - - if branch_to_use is None or tree_to_use is None: + # Decide which tree to render: prefer full crew tree, else parent branch + tree_to_use = self.current_crew_tree or crew_tree or self.current_task_branch + if tree_to_use is None: return - # Remove the thinking status node when complete, but only if it exists + # Remove the thinking status node when complete if "Thinking" in str(tool_branch.label): - try: - # Check if the node is actually in the children list - if tool_branch in branch_to_use.children: - branch_to_use.children.remove(tool_branch) - self.print(tree_to_use) - self.print() - except Exception: - # If any error occurs during removal, just continue without removing - pass + parents = [ + self.current_lite_agent_branch, + self.current_agent_branch, + self.current_task_branch, + tree_to_use, + ] + removed = False + for parent in parents: + if isinstance(parent, Tree) and tool_branch in parent.children: + parent.children.remove(tool_branch) + removed = True + break + + # Clear pointer if we just removed the current_tool_branch + if self.current_tool_branch is tool_branch: + self.current_tool_branch = None + + if removed: + self.print(tree_to_use) + self.print() def handle_llm_call_failed( self, tool_branch: Optional[Tree], error: str, crew_tree: Optional[Tree] @@ -558,8 +619,8 @@ class ConsoleFormatter: if not self.verbose: return - # Use LiteAgent branch if available, otherwise use crew tree - tree_to_use = self.current_lite_agent_branch or crew_tree + # Decide which tree to render: prefer full crew tree, else parent branch + tree_to_use = self.current_crew_tree or crew_tree or self.current_task_branch # Update tool branch if it exists if tool_branch: @@ -601,7 +662,7 @@ class ConsoleFormatter: test_label = Text() test_label.append("๐Ÿงช Test: ", style="blue bold") test_label.append(crew_name or "Crew", style="blue") - test_label.append("\n Status: ", style="white") + test_label.append("\nStatus: ", style="white") test_label.append("In Progress", style="yellow") test_tree = Tree(test_label) @@ -623,7 +684,7 @@ class ConsoleFormatter: test_label = Text() test_label.append("โœ… Test: ", style="green bold") test_label.append(crew_name or "Crew", style="green") - test_label.append("\n Status: ", style="white") + test_label.append("\nStatus: ", style="white") test_label.append("Completed", style="green bold") flow_tree.label = test_label @@ -712,7 +773,7 @@ class ConsoleFormatter: lite_agent_label = Text() lite_agent_label.append("๐Ÿค– LiteAgent: ", style="cyan bold") lite_agent_label.append(lite_agent_role, style="cyan") - lite_agent_label.append("\n Status: ", style="white") + lite_agent_label.append("\nStatus: ", style="white") lite_agent_label.append("In Progress", style="yellow") lite_agent_tree = Tree(lite_agent_label) @@ -751,7 +812,7 @@ class ConsoleFormatter: lite_agent_label = Text() lite_agent_label.append(f"{prefix} ", style=f"{style} bold") lite_agent_label.append(lite_agent_role, style=style) - lite_agent_label.append("\n Status: ", style="white") + lite_agent_label.append("Status: ", style="white") lite_agent_label.append(status_text, style=f"{style} bold") lite_agent_branch.label = lite_agent_label @@ -1004,16 +1065,20 @@ class ConsoleFormatter: if not self.verbose: return None - # Prefer LiteAgent branch if we are within a LiteAgent context - branch_to_use = self.current_lite_agent_branch or agent_branch - tree_to_use = branch_to_use or crew_tree + # Prefer LiteAgent > Agent > Task branch as the parent for reasoning + branch_to_use = ( + self.current_lite_agent_branch + or agent_branch + or self.current_task_branch + ) - if branch_to_use is None or tree_to_use is None: - # If we don't have a valid branch, default to crew_tree if provided - if crew_tree is not None: - branch_to_use = tree_to_use = crew_tree - else: - return None + # We always want to render the full crew tree when possible so the + # Live view updates coherently. Fallbacks: crew tree โ†’ branch itself. + tree_to_use = self.current_crew_tree or crew_tree or branch_to_use + + if branch_to_use is None: + # Nothing to attach to, abort + return None # Reuse existing reasoning branch if present reasoning_branch = self.current_reasoning_branch @@ -1044,8 +1109,9 @@ class ConsoleFormatter: reasoning_branch = self.current_reasoning_branch tree_to_use = ( - self.current_lite_agent_branch - or self.current_agent_branch + self.current_crew_tree + or self.current_lite_agent_branch + or self.current_task_branch or crew_tree ) @@ -1084,8 +1150,9 @@ class ConsoleFormatter: reasoning_branch = self.current_reasoning_branch tree_to_use = ( - self.current_lite_agent_branch - or self.current_agent_branch + self.current_crew_tree + or self.current_lite_agent_branch + or self.current_task_branch or crew_tree )