Lorenze/console printer nice (#3004)
Some checks failed
Notify Downstream / notify-downstream (push) Has been cancelled
Mark stale issues and pull requests / stale (push) Has been cancelled

* fix: possible fix for Thinking stuck

* feat: add agent logging events for execution tracking

- Introduced AgentLogsStartedEvent and AgentLogsExecutionEvent to enhance logging capabilities during agent execution.
- Updated CrewAgentExecutor to emit these events at the start and during execution, respectively.
- Modified EventListener to handle the new logging events and format output accordingly in the console.
- Enhanced ConsoleFormatter to display agent logs in a structured format, improving visibility of agent actions and outputs.

* drop emoji

* refactor: improve code structure and logging in LiteAgent and ConsoleFormatter

- Refactored imports in lite_agent.py for better readability.
- Enhanced guardrail property initialization in LiteAgent.
- Updated logging functionality to emit AgentLogsExecutionEvent for better tracking.
- Modified ConsoleFormatter to include tool arguments and final output in status updates.
- Improved output formatting for long text in ConsoleFormatter.

* fix tests

---------

Co-authored-by: Eduardo Chiarotti <dudumelgaco@hotmail.com>
This commit is contained in:
Lorenze Jay
2025-06-14 12:21:46 -07:00
committed by GitHub
parent 7f12e98de5
commit a5b5c8ab37
10 changed files with 1608 additions and 143 deletions

View File

@@ -25,12 +25,16 @@ from crewai.utilities.agent_utils import (
has_reached_max_iterations,
is_context_length_exceeded,
process_llm_response,
show_agent_logs,
)
from crewai.utilities.constants import MAX_LLM_RETRY, TRAINING_DATA_FILE
from crewai.utilities.logger import Logger
from crewai.utilities.tool_utils import execute_tool_and_check_finality
from crewai.utilities.training_handler import CrewTrainingHandler
from crewai.utilities.events.agent_events import (
AgentLogsStartedEvent,
AgentLogsExecutionEvent,
)
from crewai.utilities.events.crewai_event_bus import crewai_event_bus
class CrewAgentExecutor(CrewAgentExecutorMixin):
@@ -263,26 +267,32 @@ class CrewAgentExecutor(CrewAgentExecutorMixin):
"""Show logs for the start of agent execution."""
if self.agent is None:
raise ValueError("Agent cannot be None")
show_agent_logs(
printer=self._printer,
agent_role=self.agent.role,
task_description=(
getattr(self.task, "description") if self.task else "Not Found"
crewai_event_bus.emit(
self.agent,
AgentLogsStartedEvent(
agent_role=self.agent.role,
task_description=(
getattr(self.task, "description") if self.task else "Not Found"
),
verbose=self.agent.verbose
or (hasattr(self, "crew") and getattr(self.crew, "verbose", False)),
),
verbose=self.agent.verbose
or (hasattr(self, "crew") and getattr(self.crew, "verbose", False)),
)
def _show_logs(self, formatted_answer: Union[AgentAction, AgentFinish]):
"""Show logs for the agent's execution."""
if self.agent is None:
raise ValueError("Agent cannot be None")
show_agent_logs(
printer=self._printer,
agent_role=self.agent.role,
formatted_answer=formatted_answer,
verbose=self.agent.verbose
or (hasattr(self, "crew") and getattr(self.crew, "verbose", False)),
crewai_event_bus.emit(
self.agent,
AgentLogsExecutionEvent(
agent_role=self.agent.role,
formatted_answer=formatted_answer,
verbose=self.agent.verbose
or (hasattr(self, "crew") and getattr(self.crew, "verbose", False)),
),
)
def _summarize_messages(self) -> None:

View File

@@ -1,14 +1,33 @@
import asyncio
import inspect
import uuid
from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union, cast, get_args, get_origin
from typing import (
Any,
Callable,
Dict,
List,
Optional,
Tuple,
Type,
Union,
cast,
get_args,
get_origin,
)
try:
from typing import Self
except ImportError:
from typing_extensions import Self
from pydantic import BaseModel, Field, InstanceOf, PrivateAttr, model_validator, field_validator
from pydantic import (
BaseModel,
Field,
InstanceOf,
PrivateAttr,
model_validator,
field_validator,
)
from crewai.agents.agent_builder.base_agent import BaseAgent
from crewai.agents.agent_builder.utilities.base_token_process import TokenProcess
@@ -39,10 +58,10 @@ from crewai.utilities.agent_utils import (
parse_tools,
process_llm_response,
render_text_description_and_args,
show_agent_logs,
)
from crewai.utilities.converter import generate_model_description
from crewai.utilities.events.agent_events import (
AgentLogsExecutionEvent,
LiteAgentExecutionCompletedEvent,
LiteAgentExecutionErrorEvent,
LiteAgentExecutionStartedEvent,
@@ -153,9 +172,11 @@ class LiteAgent(FlowTrackable, BaseModel):
)
# Guardrail Properties
guardrail: Optional[Union[Callable[[LiteAgentOutput], Tuple[bool, Any]], str]] = Field(
default=None,
description="Function or string description of a guardrail to validate agent output"
guardrail: Optional[Union[Callable[[LiteAgentOutput], Tuple[bool, Any]], str]] = (
Field(
default=None,
description="Function or string description of a guardrail to validate agent output",
)
)
guardrail_max_retries: int = Field(
default=3, description="Maximum number of retries when guardrail fails"
@@ -181,7 +202,6 @@ class LiteAgent(FlowTrackable, BaseModel):
_guardrail: Optional[Callable] = PrivateAttr(default=None)
_guardrail_retry_count: int = PrivateAttr(default=0)
@model_validator(mode="after")
def setup_llm(self):
"""Set up the LLM and other components after initialization."""
@@ -208,17 +228,18 @@ class LiteAgent(FlowTrackable, BaseModel):
self._guardrail = self.guardrail
elif isinstance(self.guardrail, str):
from crewai.tasks.llm_guardrail import LLMGuardrail
assert isinstance(self.llm, LLM)
self._guardrail = LLMGuardrail(
description=self.guardrail, llm=self.llm
)
self._guardrail = LLMGuardrail(description=self.guardrail, llm=self.llm)
return self
@field_validator("guardrail", mode="before")
@classmethod
def validate_guardrail_function(cls, v: Optional[Union[Callable, str]]) -> Optional[Union[Callable, str]]:
def validate_guardrail_function(
cls, v: Optional[Union[Callable, str]]
) -> Optional[Union[Callable, str]]:
"""Validate that the guardrail function has the correct signature.
If v is a callable, validate that it has the correct signature.
@@ -330,9 +351,7 @@ class LiteAgent(FlowTrackable, BaseModel):
if self.response_format:
try:
# Cast to BaseModel to ensure type safety
result = self.response_format.model_validate_json(
agent_finish.output
)
result = self.response_format.model_validate_json(agent_finish.output)
if isinstance(result, BaseModel):
formatted_result = result
except Exception as e:
@@ -357,15 +376,15 @@ class LiteAgent(FlowTrackable, BaseModel):
guardrail_result = process_guardrail(
output=output,
guardrail=self._guardrail,
retry_count=self._guardrail_retry_count
retry_count=self._guardrail_retry_count,
)
if not guardrail_result.success:
if self._guardrail_retry_count >= self.guardrail_max_retries:
raise Exception(
f"Agent's guardrail failed validation after {self.guardrail_max_retries} retries. "
f"Last error: {guardrail_result.error}"
)
raise Exception(
f"Agent's guardrail failed validation after {self.guardrail_max_retries} retries. "
f"Last error: {guardrail_result.error}"
)
self._guardrail_retry_count += 1
if self.verbose:
self._printer.print(
@@ -373,10 +392,13 @@ class LiteAgent(FlowTrackable, BaseModel):
f"\n{guardrail_result.error}"
)
self._messages.append({
"role": "user",
"content": guardrail_result.error or "Guardrail validation failed"
})
self._messages.append(
{
"role": "user",
"content": guardrail_result.error
or "Guardrail validation failed",
}
)
return self._execute_core(agent_info=agent_info)
@@ -580,11 +602,13 @@ class LiteAgent(FlowTrackable, BaseModel):
def _show_logs(self, formatted_answer: Union[AgentAction, AgentFinish]):
"""Show logs for the agent's execution."""
show_agent_logs(
printer=self._printer,
agent_role=self.role,
formatted_answer=formatted_answer,
verbose=self.verbose,
crewai_event_bus.emit(
self,
AgentLogsExecutionEvent(
agent_role=self.role,
formatted_answer=formatted_answer,
verbose=self.verbose,
),
)
def _append_message(self, text: str, role: str = "assistant") -> None:

View File

@@ -102,3 +102,24 @@ class LiteAgentExecutionErrorEvent(BaseEvent):
agent_info: Dict[str, Any]
error: str
type: str = "lite_agent_execution_error"
# New logging events
class AgentLogsStartedEvent(BaseEvent):
"""Event emitted when agent logs should be shown at start"""
agent_role: str
task_description: Optional[str] = None
verbose: bool = False
type: str = "agent_logs_started"
class AgentLogsExecutionEvent(BaseEvent):
"""Event emitted when agent logs should be shown during execution"""
agent_role: str
formatted_answer: Any
verbose: bool = False
type: str = "agent_logs_execution"
model_config = {"arbitrary_types_allowed": True}

View File

@@ -27,6 +27,8 @@ from crewai.utilities.events.utils.console_formatter import ConsoleFormatter
from .agent_events import (
AgentExecutionCompletedEvent,
AgentExecutionStartedEvent,
AgentLogsStartedEvent,
AgentLogsExecutionEvent,
LiteAgentExecutionCompletedEvent,
LiteAgentExecutionErrorEvent,
LiteAgentExecutionStartedEvent,
@@ -108,6 +110,7 @@ class EventListener(BaseEventListener):
event.crew_name or "Crew",
source.id,
"completed",
final_string_output,
)
@crewai_event_bus.on(CrewKickoffFailedEvent)
@@ -286,13 +289,14 @@ class EventListener(BaseEventListener):
if isinstance(source, LLM):
self.formatter.handle_llm_tool_usage_started(
event.tool_name,
event.tool_args,
)
else:
self.formatter.handle_tool_usage_started(
self.formatter.current_agent_branch,
event.tool_name,
self.formatter.current_crew_tree,
)
self.formatter.current_crew_tree,
)
@crewai_event_bus.on(ToolUsageFinishedEvent)
def on_tool_usage_finished(source, event: ToolUsageFinishedEvent):
@@ -320,16 +324,20 @@ class EventListener(BaseEventListener):
event.tool_name,
event.error,
self.formatter.current_crew_tree,
)
)
# ----------- LLM EVENTS -----------
@crewai_event_bus.on(LLMCallStartedEvent)
def on_llm_call_started(source, event: LLMCallStartedEvent):
self.formatter.handle_llm_call_started(
# Capture the returned tool branch and update the current_tool_branch reference
thinking_branch = self.formatter.handle_llm_call_started(
self.formatter.current_agent_branch,
self.formatter.current_crew_tree,
)
# Update the formatter's current_tool_branch to ensure proper cleanup
if thinking_branch is not None:
self.formatter.current_tool_branch = thinking_branch
@crewai_event_bus.on(LLMCallCompletedEvent)
def on_llm_call_completed(source, event: LLMCallCompletedEvent):
@@ -462,5 +470,23 @@ class EventListener(BaseEventListener):
self.formatter.current_crew_tree,
)
# ----------- AGENT LOGGING EVENTS -----------
@crewai_event_bus.on(AgentLogsStartedEvent)
def on_agent_logs_started(source, event: AgentLogsStartedEvent):
self.formatter.handle_agent_logs_started(
event.agent_role,
event.task_description,
event.verbose,
)
@crewai_event_bus.on(AgentLogsExecutionEvent)
def on_agent_logs_execution(source, event: AgentLogsExecutionEvent):
self.formatter.handle_agent_logs_execution(
event.agent_role,
event.formatted_answer,
event.verbose,
)
event_listener = EventListener()

View File

@@ -5,6 +5,7 @@ from rich.panel import Panel
from rich.text import Text
from rich.tree import Tree
from rich.live import Live
from rich.syntax import Syntax
class ConsoleFormatter:
@@ -40,7 +41,12 @@ class ConsoleFormatter:
)
def create_status_content(
self, title: str, name: str, status_style: str = "blue", **fields
self,
title: str,
name: str,
status_style: str = "blue",
tool_args: Dict[str, Any] | str = "",
**fields,
) -> Text:
"""Create standardized status content with consistent formatting."""
content = Text()
@@ -53,6 +59,8 @@ class ConsoleFormatter:
content.append(
f"{value}\n", style=fields.get(f"{label}_style", status_style)
)
content.append("Tool Args: ", style="white")
content.append(f"{tool_args}\n", style=status_style)
return content
@@ -152,6 +160,7 @@ class ConsoleFormatter:
crew_name: str,
source_id: str,
status: str = "completed",
final_string_output: str = "",
) -> None:
"""Handle crew tree updates with consistent formatting."""
if not self.verbose or tree is None:
@@ -183,6 +192,7 @@ class ConsoleFormatter:
style,
ID=source_id,
)
content.append(f"Final Output: {final_string_output}\n", style="white")
self.print_panel(content, title, style)
@@ -455,12 +465,19 @@ class ConsoleFormatter:
def handle_llm_tool_usage_started(
self,
tool_name: str,
tool_args: Dict[str, Any] | str,
):
tree = self.get_llm_tree(tool_name)
self.add_tree_node(tree, "🔄 Tool Usage Started", "green")
self.print(tree)
# Create status content for the tool usage
content = self.create_status_content(
"Tool Usage Started", tool_name, Status="In Progress", tool_args=tool_args
)
# Create and print the panel
self.print_panel(content, "Tool Usage", "green")
self.print()
return tree
# Still return the tree for compatibility with existing code
return self.get_llm_tree(tool_name)
def handle_llm_tool_usage_finished(
self,
@@ -491,6 +508,7 @@ class ConsoleFormatter:
agent_branch: Optional[Tree],
tool_name: str,
crew_tree: Optional[Tree],
tool_args: Dict[str, Any] | str = "",
) -> Optional[Tree]:
"""Handle tool usage started event."""
if not self.verbose:
@@ -498,9 +516,7 @@ class ConsoleFormatter:
# Parent for tool usage: LiteAgent > Agent > Task
branch_to_use = (
self.current_lite_agent_branch
or agent_branch
or self.current_task_branch
self.current_lite_agent_branch or agent_branch or self.current_task_branch
)
# Render full crew tree when available for consistent live updates
@@ -609,9 +625,7 @@ class ConsoleFormatter:
# Parent for tool usage: LiteAgent > Agent > Task
branch_to_use = (
self.current_lite_agent_branch
or agent_branch
or self.current_task_branch
self.current_lite_agent_branch or agent_branch or self.current_task_branch
)
# Render full crew tree when available for consistent live updates
@@ -625,14 +639,21 @@ class ConsoleFormatter:
return None
# Only add thinking status if we don't have a current tool branch
if self.current_tool_branch is None:
# or if the current tool branch is not a thinking node
should_add_thinking = self.current_tool_branch is None or "Thinking" not in str(
self.current_tool_branch.label
)
if should_add_thinking:
tool_branch = branch_to_use.add("")
self.update_tree_label(tool_branch, "🧠", "Thinking...", "blue")
self.current_tool_branch = tool_branch
self.print(tree_to_use)
self.print()
return tool_branch
return None
# Return the existing tool branch if it's already a thinking node
return self.current_tool_branch
def handle_llm_call_completed(
self,
@@ -641,7 +662,7 @@ class ConsoleFormatter:
crew_tree: Optional[Tree],
) -> None:
"""Handle LLM call completed event."""
if not self.verbose or tool_branch is None:
if not self.verbose:
return
# Decide which tree to render: prefer full crew tree, else parent branch
@@ -649,23 +670,50 @@ class ConsoleFormatter:
if tree_to_use is None:
return
# Remove the thinking status node when complete
if "Thinking" in str(tool_branch.label):
# Try to remove the thinking status node - first try the provided tool_branch
thinking_branch_to_remove = None
removed = False
# Method 1: Use the provided tool_branch if it's a thinking node
if tool_branch is not None and "Thinking" in str(tool_branch.label):
thinking_branch_to_remove = tool_branch
# Method 2: Fallback - search for any thinking node if tool_branch is None or not thinking
if thinking_branch_to_remove is None:
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)
if isinstance(parent, Tree):
for child in parent.children:
if "Thinking" in str(child.label):
thinking_branch_to_remove = child
break
if thinking_branch_to_remove:
break
# Remove the thinking node if found
if thinking_branch_to_remove:
parents = [
self.current_lite_agent_branch,
self.current_agent_branch,
self.current_task_branch,
tree_to_use,
]
for parent in parents:
if (
isinstance(parent, Tree)
and thinking_branch_to_remove in parent.children
):
parent.children.remove(thinking_branch_to_remove)
removed = True
break
# Clear pointer if we just removed the current_tool_branch
if self.current_tool_branch is tool_branch:
if self.current_tool_branch is thinking_branch_to_remove:
self.current_tool_branch = None
if removed:
@@ -682,9 +730,36 @@ class ConsoleFormatter:
# 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:
tool_branch.label = Text("❌ LLM Failed", style="red bold")
# Find the thinking branch to update (similar to completion logic)
thinking_branch_to_update = None
# Method 1: Use the provided tool_branch if it's a thinking node
if tool_branch is not None and "Thinking" in str(tool_branch.label):
thinking_branch_to_update = tool_branch
# Method 2: Fallback - search for any thinking node if tool_branch is None or not thinking
if thinking_branch_to_update is None:
parents = [
self.current_lite_agent_branch,
self.current_agent_branch,
self.current_task_branch,
tree_to_use,
]
for parent in parents:
if isinstance(parent, Tree):
for child in parent.children:
if "Thinking" in str(child.label):
thinking_branch_to_update = child
break
if thinking_branch_to_update:
break
# Update the thinking branch to show failure
if thinking_branch_to_update:
thinking_branch_to_update.label = Text("❌ LLM Failed", style="red bold")
# Clear the current_tool_branch reference
if self.current_tool_branch is thinking_branch_to_update:
self.current_tool_branch = None
if tree_to_use:
self.print(tree_to_use)
self.print()
@@ -1127,9 +1202,7 @@ class ConsoleFormatter:
# 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
self.current_lite_agent_branch or agent_branch or self.current_task_branch
)
# We always want to render the full crew tree when possible so the
@@ -1176,7 +1249,9 @@ class ConsoleFormatter:
)
style = "green" if ready else "yellow"
status_text = "Reasoning Completed" if ready else "Reasoning Completed (Not Ready)"
status_text = (
"Reasoning Completed" if ready else "Reasoning Completed (Not Ready)"
)
if reasoning_branch is not None:
self.update_tree_label(reasoning_branch, "", status_text, style)
@@ -1233,3 +1308,149 @@ class ConsoleFormatter:
# Clear stored branch after failure
self.current_reasoning_branch = None
# ----------- AGENT LOGGING EVENTS -----------
def handle_agent_logs_started(
self,
agent_role: str,
task_description: Optional[str] = None,
verbose: bool = False,
) -> None:
"""Handle agent logs started event."""
if not verbose:
return
agent_role = agent_role.split("\n")[0]
# Create panel content
content = Text()
content.append("Agent: ", style="white")
content.append(f"{agent_role}", style="bright_green bold")
if task_description:
content.append("\n\nTask: ", style="white")
content.append(f"{task_description}", style="bright_green")
# Create and display the panel
agent_panel = Panel(
content,
title="🤖 Agent Started",
border_style="magenta",
padding=(1, 2),
)
self.print(agent_panel)
self.print()
def handle_agent_logs_execution(
self,
agent_role: str,
formatted_answer: Any,
verbose: bool = False,
) -> None:
"""Handle agent logs execution event."""
if not verbose:
return
from crewai.agents.parser import AgentAction, AgentFinish
import json
import re
agent_role = agent_role.split("\n")[0]
if isinstance(formatted_answer, AgentAction):
thought = re.sub(r"\n+", "\n", formatted_answer.thought)
formatted_json = json.dumps(
formatted_answer.tool_input,
indent=2,
ensure_ascii=False,
)
# Create content for the action panel
content = Text()
content.append("Agent: ", style="white")
content.append(f"{agent_role}\n\n", style="bright_green bold")
if thought and thought != "":
content.append("Thought: ", style="white")
content.append(f"{thought}\n\n", style="bright_green")
content.append("Using Tool: ", style="white")
content.append(f"{formatted_answer.tool}\n\n", style="bright_green bold")
content.append("Tool Input:\n", style="white")
# Create a syntax-highlighted JSON code block
json_syntax = Syntax(
formatted_json,
"json",
theme="monokai",
line_numbers=False,
background_color="default",
)
content.append("\n")
# Create separate panels for better organization
main_content = Text()
main_content.append("Agent: ", style="white")
main_content.append(f"{agent_role}\n\n", style="bright_green bold")
if thought and thought != "":
main_content.append("Thought: ", style="white")
main_content.append(f"{thought}\n\n", style="bright_green")
main_content.append("Using Tool: ", style="white")
main_content.append(f"{formatted_answer.tool}", style="bright_green bold")
# Create the main action panel
action_panel = Panel(
main_content,
title="🔧 Agent Tool Execution",
border_style="magenta",
padding=(1, 2),
)
# Create the JSON input panel
input_panel = Panel(
json_syntax,
title="Tool Input",
border_style="blue",
padding=(1, 2),
)
# Create tool output content with better formatting
output_text = str(formatted_answer.result)
if len(output_text) > 2000:
output_text = output_text[:1997] + "..."
output_panel = Panel(
Text(output_text, style="bright_green"),
title="Tool Output",
border_style="green",
padding=(1, 2),
)
# Print all panels
self.print(action_panel)
self.print(input_panel)
self.print(output_panel)
self.print()
elif isinstance(formatted_answer, AgentFinish):
# Create content for the finish panel
content = Text()
content.append("Agent: ", style="white")
content.append(f"{agent_role}\n\n", style="bright_green bold")
content.append("Final Answer:\n", style="white")
content.append(f"{formatted_answer.output}", style="bright_green")
# Create and display the finish panel
finish_panel = Panel(
content,
title="✅ Agent Final Answer",
border_style="green",
padding=(1, 2),
)
self.print(finish_panel)
self.print()