Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RichConsole: Prettify m1 CLI console using rich #4806 #5123

Merged
merged 7 commits into from
Jan 24, 2025
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions python/packages/autogen-ext/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ semantic-kernel-all = [
"semantic-kernel[google,hugging_face,mistralai,ollama,onnx,anthropic,usearch,pandas,aws,dapr]>=1.17.1",
]

rich = ["rich>=13.9.4"]

[tool.hatch.build.targets.wheel]
packages = ["src/autogen_ext"]

Expand Down
7 changes: 7 additions & 0 deletions python/packages/autogen-ext/src/autogen_ext/ui/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"""
This module implements utility classes for formatting/printing agent messages.
"""

from ._rich_console import RichConsole

Check warning on line 5 in python/packages/autogen-ext/src/autogen_ext/ui/__init__.py

View check run for this annotation

Codecov / codecov/patch

python/packages/autogen-ext/src/autogen_ext/ui/__init__.py#L5

Added line #L5 was not covered by tests

__all__ = ["RichConsole"]

Check warning on line 7 in python/packages/autogen-ext/src/autogen_ext/ui/__init__.py

View check run for this annotation

Codecov / codecov/patch

python/packages/autogen-ext/src/autogen_ext/ui/__init__.py#L7

Added line #L7 was not covered by tests
219 changes: 219 additions & 0 deletions python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
import asyncio
import os
import sys
import time
from typing import (

Check warning on line 5 in python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py

View check run for this annotation

Codecov / codecov/patch

python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py#L1-L5

Added lines #L1 - L5 were not covered by tests
AsyncGenerator,
Awaitable,
List,
Optional,
Tuple,
TypeVar,
cast,
)

from autogen_agentchat.base import Response, TaskResult
from autogen_agentchat.messages import (

Check warning on line 16 in python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py

View check run for this annotation

Codecov / codecov/patch

python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py#L15-L16

Added lines #L15 - L16 were not covered by tests
AgentEvent,
ChatMessage,
MultiModalMessage,
UserInputRequestedEvent,
)
from autogen_agentchat.ui._console import UserInputManager
from autogen_core import Image
from autogen_core.models import RequestUsage
from rich.align import AlignMethod
from rich.console import Console
from rich.panel import Panel

Check warning on line 27 in python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py

View check run for this annotation

Codecov / codecov/patch

python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py#L22-L27

Added lines #L22 - L27 were not covered by tests

AGENT_COLORS = {

Check warning on line 29 in python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py

View check run for this annotation

Codecov / codecov/patch

python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py#L29

Added line #L29 was not covered by tests
"user": "bright_green",
"MagenticOneOrchestrator": "bright_blue",
"WebSurfer": "bright_yellow",
"FileSurfer": "bright_cyan",
"Coder": "bright_magenta",
"Executor": "bright_red",
}
DEFAULT_AGENT_COLOR = "white"

Check warning on line 37 in python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py

View check run for this annotation

Codecov / codecov/patch

python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py#L37

Added line #L37 was not covered by tests

AGENT_ALIGNMENTS: dict[str, AlignMethod] = {"user": "right", "MagenticOneOrchestrator": "center"}
DEFAULT_AGENT_ALIGNMENT: AlignMethod = "left"

Check warning on line 40 in python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py

View check run for this annotation

Codecov / codecov/patch

python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py#L39-L40

Added lines #L39 - L40 were not covered by tests


def _is_running_in_iterm() -> bool:
return os.getenv("TERM_PROGRAM") == "iTerm.app"

Check warning on line 44 in python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py

View check run for this annotation

Codecov / codecov/patch

python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py#L43-L44

Added lines #L43 - L44 were not covered by tests


def _is_output_a_tty() -> bool:
return sys.stdout.isatty()

Check warning on line 48 in python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py

View check run for this annotation

Codecov / codecov/patch

python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py#L47-L48

Added lines #L47 - L48 were not covered by tests


T = TypeVar("T", bound=TaskResult | Response)

Check warning on line 51 in python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py

View check run for this annotation

Codecov / codecov/patch

python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py#L51

Added line #L51 was not covered by tests


def aprint(output: str, end: str = "\n") -> Awaitable[None]:
return asyncio.to_thread(print, output, end=end)

Check warning on line 55 in python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py

View check run for this annotation

Codecov / codecov/patch

python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py#L54-L55

Added lines #L54 - L55 were not covered by tests


def _extract_message_content(message: AgentEvent | ChatMessage) -> Tuple[List[str], List[Image]]:
if isinstance(message, MultiModalMessage):
text_parts = [item for item in message.content if isinstance(item, str)]
image_parts = [item for item in message.content if isinstance(item, Image)]

Check warning on line 61 in python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py

View check run for this annotation

Codecov / codecov/patch

python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py#L58-L61

Added lines #L58 - L61 were not covered by tests
else:
text_parts = [str(message.content)]
image_parts = []
return text_parts, image_parts

Check warning on line 65 in python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py

View check run for this annotation

Codecov / codecov/patch

python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py#L63-L65

Added lines #L63 - L65 were not covered by tests


async def _aprint_panel(console: Console, text: str, title: str) -> None:
color = AGENT_COLORS.get(title, DEFAULT_AGENT_COLOR)
title_align = AGENT_ALIGNMENTS.get(title, DEFAULT_AGENT_ALIGNMENT)

Check warning on line 70 in python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py

View check run for this annotation

Codecov / codecov/patch

python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py#L68-L70

Added lines #L68 - L70 were not covered by tests

await asyncio.to_thread(

Check warning on line 72 in python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py

View check run for this annotation

Codecov / codecov/patch

python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py#L72

Added line #L72 was not covered by tests
console.print,
Panel(
text,
title=title,
title_align=title_align,
border_style=color,
),
)


async def _aprint_message_content(

Check warning on line 83 in python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py

View check run for this annotation

Codecov / codecov/patch

python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py#L83

Added line #L83 was not covered by tests
console: Console,
text_parts: List[str],
image_parts: List[Image],
source: str,
*,
render_image_iterm: bool = False,
) -> None:
if text_parts:
await _aprint_panel(console, "\n".join(text_parts), source)

Check warning on line 92 in python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py

View check run for this annotation

Codecov / codecov/patch

python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py#L91-L92

Added lines #L91 - L92 were not covered by tests

for img in image_parts:
if render_image_iterm:
await aprint(_image_to_iterm(img))

Check warning on line 96 in python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py

View check run for this annotation

Codecov / codecov/patch

python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py#L94-L96

Added lines #L94 - L96 were not covered by tests
else:
await aprint("<image>\n")

Check warning on line 98 in python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py

View check run for this annotation

Codecov / codecov/patch

python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py#L98

Added line #L98 was not covered by tests


async def RichConsole(

Check warning on line 101 in python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py

View check run for this annotation

Codecov / codecov/patch

python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py#L101

Added line #L101 was not covered by tests
stream: AsyncGenerator[AgentEvent | ChatMessage | T, None],
*,
no_inline_images: bool = False,
output_stats: bool = False,
user_input_manager: UserInputManager | None = None,
) -> T:
"""
Consumes the message stream from :meth:`~autogen_agentchat.base.TaskRunner.run_stream`
or :meth:`~autogen_agentchat.base.ChatAgent.on_messages_stream` and renders the messages to the console.
Returns the last processed TaskResult or Response.

.. note::

`output_stats` is experimental and the stats may not be accurate.
It will be improved in future releases.

Args:
stream (AsyncGenerator[AgentEvent | ChatMessage | TaskResult, None] | AsyncGenerator[AgentEvent | ChatMessage | Response, None]): Message stream to render.
This can be from :meth:`~autogen_agentchat.base.TaskRunner.run_stream` or :meth:`~autogen_agentchat.base.ChatAgent.on_messages_stream`.
no_inline_images (bool, optional): If terminal is iTerm2 will render images inline. Use this to disable this behavior. Defaults to False.
output_stats (bool, optional): (Experimental) If True, will output a summary of the messages and inline token usage info. Defaults to False.

Returns:
last_processed: A :class:`~autogen_agentchat.base.TaskResult` if the stream is from :meth:`~autogen_agentchat.base.TaskRunner.run_stream`
or a :class:`~autogen_agentchat.base.Response` if the stream is from :meth:`~autogen_agentchat.base.ChatAgent.on_messages_stream`.
"""
render_image_iterm = _is_running_in_iterm() and _is_output_a_tty() and not no_inline_images
start_time = time.time()
total_usage = RequestUsage(prompt_tokens=0, completion_tokens=0)
rich_console = Console()

Check warning on line 131 in python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py

View check run for this annotation

Codecov / codecov/patch

python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py#L128-L131

Added lines #L128 - L131 were not covered by tests

last_processed: Optional[T] = None

Check warning on line 133 in python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py

View check run for this annotation

Codecov / codecov/patch

python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py#L133

Added line #L133 was not covered by tests

async for message in stream:
if isinstance(message, TaskResult):
duration = time.time() - start_time
if output_stats:
output = (

Check warning on line 139 in python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py

View check run for this annotation

Codecov / codecov/patch

python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py#L135-L139

Added lines #L135 - L139 were not covered by tests
f"Number of messages: {len(message.messages)}\n"
f"Finish reason: {message.stop_reason}\n"
f"Total prompt tokens: {total_usage.prompt_tokens}\n"
f"Total completion tokens: {total_usage.completion_tokens}\n"
f"Duration: {duration:.2f} seconds\n"
)
await _aprint_panel(rich_console, output, "Summary")

Check warning on line 146 in python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py

View check run for this annotation

Codecov / codecov/patch

python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py#L146

Added line #L146 was not covered by tests

last_processed = message # type: ignore

Check warning on line 148 in python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py

View check run for this annotation

Codecov / codecov/patch

python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py#L148

Added line #L148 was not covered by tests

elif isinstance(message, Response):
duration = time.time() - start_time

Check warning on line 151 in python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py

View check run for this annotation

Codecov / codecov/patch

python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py#L150-L151

Added lines #L150 - L151 were not covered by tests

# Print final response.
text_parts, image_parts = _extract_message_content(message.chat_message)
if message.chat_message.models_usage:
if output_stats:
text_parts.append(

Check warning on line 157 in python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py

View check run for this annotation

Codecov / codecov/patch

python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py#L154-L157

Added lines #L154 - L157 were not covered by tests
f"[Prompt tokens: {message.chat_message.models_usage.prompt_tokens}, Completion tokens: {message.chat_message.models_usage.completion_tokens}]"
)
total_usage.completion_tokens += message.chat_message.models_usage.completion_tokens
total_usage.prompt_tokens += message.chat_message.models_usage.prompt_tokens

Check warning on line 161 in python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py

View check run for this annotation

Codecov / codecov/patch

python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py#L160-L161

Added lines #L160 - L161 were not covered by tests

await _aprint_message_content(

Check warning on line 163 in python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py

View check run for this annotation

Codecov / codecov/patch

python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py#L163

Added line #L163 was not covered by tests
rich_console,
text_parts,
image_parts,
message.chat_message.source,
render_image_iterm=render_image_iterm,
)

# Print summary.
if output_stats:
num_inner_messages = len(message.inner_messages) if message.inner_messages is not None else 0
output = (

Check warning on line 174 in python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py

View check run for this annotation

Codecov / codecov/patch

python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py#L172-L174

Added lines #L172 - L174 were not covered by tests
f"Number of inner messages: {num_inner_messages}\n"
f"Total prompt tokens: {total_usage.prompt_tokens}\n"
f"Total completion tokens: {total_usage.completion_tokens}\n"
f"Duration: {duration:.2f} seconds\n"
)
await _aprint_panel(rich_console, output, "Summary")

Check warning on line 180 in python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py

View check run for this annotation

Codecov / codecov/patch

python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py#L180

Added line #L180 was not covered by tests

# mypy ignore
last_processed = message # type: ignore

Check warning on line 183 in python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py

View check run for this annotation

Codecov / codecov/patch

python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py#L183

Added line #L183 was not covered by tests
# We don't want to print UserInputRequestedEvent messages, we just use them to signal the user input event.
elif isinstance(message, UserInputRequestedEvent):
if user_input_manager is not None:
user_input_manager.notify_event_received(message.request_id)

Check warning on line 187 in python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py

View check run for this annotation

Codecov / codecov/patch

python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py#L185-L187

Added lines #L185 - L187 were not covered by tests
else:
# Cast required for mypy to be happy
message = cast(AgentEvent | ChatMessage, message) # type: ignore

Check warning on line 190 in python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py

View check run for this annotation

Codecov / codecov/patch

python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py#L190

Added line #L190 was not covered by tests

text_parts, image_parts = _extract_message_content(message)

Check warning on line 192 in python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py

View check run for this annotation

Codecov / codecov/patch

python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py#L192

Added line #L192 was not covered by tests
# Add usage stats if needed
if message.models_usage:
if output_stats:
text_parts.append(

Check warning on line 196 in python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py

View check run for this annotation

Codecov / codecov/patch

python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py#L194-L196

Added lines #L194 - L196 were not covered by tests
f"[Prompt tokens: {message.models_usage.prompt_tokens}, Completion tokens: {message.models_usage.completion_tokens}]"
)
total_usage.completion_tokens += message.models_usage.completion_tokens
total_usage.prompt_tokens += message.models_usage.prompt_tokens

Check warning on line 200 in python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py

View check run for this annotation

Codecov / codecov/patch

python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py#L199-L200

Added lines #L199 - L200 were not covered by tests

await _aprint_message_content(

Check warning on line 202 in python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py

View check run for this annotation

Codecov / codecov/patch

python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py#L202

Added line #L202 was not covered by tests
rich_console,
text_parts,
image_parts,
message.source,
render_image_iterm=render_image_iterm,
)

if last_processed is None:
raise ValueError("No TaskResult or Response was processed.")

Check warning on line 211 in python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py

View check run for this annotation

Codecov / codecov/patch

python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py#L210-L211

Added lines #L210 - L211 were not covered by tests

return last_processed

Check warning on line 213 in python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py

View check run for this annotation

Codecov / codecov/patch

python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py#L213

Added line #L213 was not covered by tests


# iTerm2 image rendering protocol: https://iterm2.com/documentation-images.html
def _image_to_iterm(image: Image) -> str:
image_data = image.to_base64()
return f"\033]1337;File=inline=1:{image_data}\a\n"

Check warning on line 219 in python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py

View check run for this annotation

Codecov / codecov/patch

python/packages/autogen-ext/src/autogen_ext/ui/_rich_console.py#L217-L219

Added lines #L217 - L219 were not covered by tests
2 changes: 1 addition & 1 deletion python/packages/magentic-one-cli/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ classifiers = [
]
dependencies = [
"autogen-agentchat>=0.4.2,<0.5",
"autogen-ext[openai,magentic-one]>=0.4.2,<0.5",
"autogen-ext[openai,magentic-one,rich]>=0.4.2,<0.5",
]

[project.scripts]
Expand Down
22 changes: 17 additions & 5 deletions python/packages/magentic-one-cli/src/magentic_one_cli/_m1.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from autogen_core import CancellationToken
from autogen_ext.models.openai import OpenAIChatCompletionClient
from autogen_ext.teams.magentic_one import MagenticOne
from autogen_ext.ui import RichConsole

# Suppress warnings about the requests.Session() not being closed
warnings.filterwarnings(action="ignore", message="unclosed", category=ResourceWarning)
Expand All @@ -24,16 +25,18 @@ def main() -> None:
Command-line interface for running a complex task using MagenticOne.

This script accepts a single task string and an optional flag to disable
human-in-the-loop mode. It initializes the necessary clients and runs the
task using the MagenticOne class.
human-in-the-loop mode and enable rich console output. It initializes the
necessary clients and runs the task using the MagenticOne class.

Arguments:
task (str): The task to be executed by MagenticOne.
--no-hil: Optional flag to disable human-in-the-loop mode.
--rich: Optional flag to enable rich console output.

Example usage:
python magentic_one_cli.py "example task"
python magentic_one_cli.py --no-hil "example task"
python magentic_one_cli.py --rich "example task"
"""
parser = argparse.ArgumentParser(
description=(
Expand All @@ -43,16 +46,25 @@ def main() -> None:
)
parser.add_argument("task", type=str, nargs=1, help="The task to be executed by MagenticOne.")
parser.add_argument("--no-hil", action="store_true", help="Disable human-in-the-loop mode.")
parser.add_argument(
"--rich",
action="store_true",
help="Enable rich console output",
)
args = parser.parse_args()

async def run_task(task: str, hil_mode: bool) -> None:
async def run_task(task: str, hil_mode: bool, use_rich_console: bool) -> None:
input_manager = UserInputManager(callback=cancellable_input)
client = OpenAIChatCompletionClient(model="gpt-4o")
m1 = MagenticOne(client=client, hil_mode=hil_mode, input_func=input_manager.get_wrapped_callback())
await Console(m1.run_stream(task=task), output_stats=False, user_input_manager=input_manager)

if use_rich_console:
await RichConsole(m1.run_stream(task=task), output_stats=False, user_input_manager=input_manager)
else:
await Console(m1.run_stream(task=task), output_stats=False, user_input_manager=input_manager)

task = args.task[0]
asyncio.run(run_task(task, not args.no_hil))
asyncio.run(run_task(task, not args.no_hil, args.rich))


if __name__ == "__main__":
Expand Down
8 changes: 6 additions & 2 deletions python/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading