Coverage for src/lite_agent/chat_display.py: 76%
420 statements
« prev ^ index » next coverage.py v7.10.5, created at 2025-08-25 22:58 +0900
« prev ^ index » next coverage.py v7.10.5, created at 2025-08-25 22:58 +0900
1"""
2Chat display utilities for lite-agent.
4This module provides utilities to beautifully display chat history using the rich library.
5It supports all message types including user messages, assistant messages, function calls,
6and function call outputs.
7"""
9import json
10import time
11from collections.abc import Callable
12from datetime import datetime, timedelta, timezone
14try:
15 from zoneinfo import ZoneInfo
16except ImportError:
17 ZoneInfo = None
19from dataclasses import dataclass
21from rich.console import Console
22from rich.table import Table
24from lite_agent.types import (
25 AgentAssistantMessage,
26 AgentSystemMessage,
27 AgentUserMessage,
28 AssistantMessageMeta,
29 AssistantToolCall,
30 AssistantToolCallResult,
31 BasicMessageMeta,
32 FlexibleRunnerMessage,
33 LLMResponseMeta,
34 NewAssistantMessage,
35 NewMessage,
36 NewSystemMessage,
37 NewUserMessage,
38 RunnerMessages,
39)
42@dataclass
43class DisplayConfig:
44 """消息显示配置。"""
46 console: Console | None = None
47 show_indices: bool = True
48 show_timestamps: bool = True
49 max_content_length: int = 1000
50 local_timezone: timezone | str | None = None
53@dataclass
54class MessageContext:
55 """消息显示上下文。"""
57 console: Console
58 index_str: str
59 timestamp_str: str
60 max_content_length: int
61 truncate_content: Callable[[str, int], str]
64def _get_local_timezone() -> timezone:
65 """
66 检测并返回用户本地时区。
68 Returns:
69 用户的本地时区对象
70 """
71 # 获取本地时区偏移(秒)
72 offset_seconds = -time.timezone if time.daylight == 0 else -time.altzone
73 # 转换为 timezone 对象
74 return timezone(timedelta(seconds=offset_seconds))
77def _get_timezone_by_name(timezone_name: str) -> timezone: # noqa: PLR0911
78 """
79 根据时区名称获取时区对象。
81 Args:
82 timezone_name: 时区名称,支持:
83 - "local": 自动检测本地时区
84 - "UTC": UTC 时区
85 - "+8", "-5": UTC 偏移量(小时)
86 - "Asia/Shanghai", "America/New_York": IANA 时区名称(需要 zoneinfo)
88 Returns:
89 对应的时区对象
90 """
91 if timezone_name.lower() == "local":
92 return _get_local_timezone()
93 if timezone_name.upper() == "UTC":
94 return timezone.utc
95 if timezone_name.startswith(("+", "-")):
96 # 解析 UTC 偏移量,如 "+8", "-5"
97 try:
98 hours = int(timezone_name)
99 return timezone(timedelta(hours=hours))
100 except ValueError:
101 return _get_local_timezone()
102 # 尝试使用 zoneinfo (Python 3.9+)
103 elif ZoneInfo is not None:
104 try:
105 zone_info = ZoneInfo(timezone_name)
106 # 转换为 timezone 对象
107 return timezone(zone_info.utcoffset(datetime.now(timezone.utc)) or timedelta(0))
108 except Exception:
109 # 如果不支持 zoneinfo,返回本地时区
110 return _get_local_timezone()
111 else:
112 return _get_local_timezone()
115def _format_timestamp(
116 dt: datetime | None = None,
117 *,
118 local_timezone: timezone | None = None,
119 format_str: str = "%H:%M:%S",
120) -> str:
121 """
122 格式化时间戳,自动转换为本地时区。
124 Args:
125 dt: 要格式化的 datetime 对象,如果为 None 则使用当前时间
126 local_timezone: 本地时区,如果为 None 则自动检测
127 format_str: 时间格式字符串
129 Returns:
130 格式化后的时间字符串
131 """
132 if dt is None:
133 dt = datetime.now(timezone.utc)
135 if local_timezone is None:
136 local_timezone = _get_local_timezone()
138 # 如果 datetime 对象没有时区信息,假设为 UTC
139 if dt.tzinfo is None:
140 dt = dt.replace(tzinfo=timezone.utc)
142 # 转换到本地时区
143 local_dt = dt.astimezone(local_timezone)
144 return local_dt.strftime(format_str)
147def build_chat_summary_table(messages: RunnerMessages) -> Table:
148 """
149 创建聊天记录摘要表格。
151 Args:
152 messages: 要汇总的消息列表
154 Returns:
155 Rich Table 对象,包含消息统计信息
156 """
157 table = Table(title="Chat Summary")
158 table.add_column("Message Type", style="cyan")
159 table.add_column("Count", justify="right", style="green")
161 # 统计各种消息类型和 meta 数据
162 counts, meta_stats = _analyze_messages(messages)
164 # 只显示计数大于0的类型
165 for msg_type, count in counts.items():
166 if count > 0:
167 table.add_row(msg_type, str(count))
169 table.add_row("[bold]Total[/bold]", f"[bold]{len(messages)}[/bold]")
171 # 添加 meta 数据统计
172 _add_meta_stats_to_table(table, meta_stats)
174 return table
177def _analyze_messages(messages: RunnerMessages) -> tuple[dict[str, int], dict[str, int | float]]:
178 """
179 分析消息并返回统计信息。
181 Args:
182 messages: 要分析的消息列表
184 Returns:
185 消息计数和 meta 数据统计信息的元组
186 """
187 counts = {
188 "User": 0,
189 "Assistant": 0,
190 "System": 0,
191 "Function Call": 0,
192 "Function Output": 0,
193 "Unknown": 0,
194 }
196 # 统计 meta 数据
197 total_input_tokens = 0
198 total_output_tokens = 0
199 total_latency_ms = 0
200 total_output_time_ms = 0
201 assistant_with_meta_count = 0
203 for message in messages:
204 _update_message_counts(message, counts)
206 # 收集 meta 数据
207 if _is_assistant_message(message):
208 meta_data = _extract_meta_data(message, total_input_tokens, total_output_tokens, total_latency_ms, total_output_time_ms)
209 if meta_data:
210 assistant_with_meta_count += 1
211 total_input_tokens, total_output_tokens, total_latency_ms, total_output_time_ms = meta_data
213 # 转换为正确的类型
214 meta_stats_typed: dict[str, int | float] = {
215 "total_input_tokens": float(total_input_tokens),
216 "total_output_tokens": float(total_output_tokens),
217 "total_latency_ms": float(total_latency_ms),
218 "total_output_time_ms": float(total_output_time_ms),
219 "assistant_with_meta_count": float(assistant_with_meta_count),
220 }
221 return counts, meta_stats_typed
224def _update_message_counts(message: FlexibleRunnerMessage, counts: dict[str, int]) -> None:
225 """更新消息计数。"""
226 # Handle new message format first
227 if isinstance(message, NewUserMessage):
228 counts["User"] += 1
229 elif isinstance(message, NewAssistantMessage):
230 counts["Assistant"] += 1
231 # Count tool calls and outputs within the assistant message
232 for content_item in message.content:
233 if isinstance(content_item, AssistantToolCall):
234 counts["Function Call"] += 1
235 elif isinstance(content_item, AssistantToolCallResult):
236 counts["Function Output"] += 1
237 elif isinstance(message, NewSystemMessage):
238 counts["System"] += 1
239 # Handle legacy message format
240 elif isinstance(message, AgentUserMessage) or (isinstance(message, dict) and message.get("role") == "user"):
241 counts["User"] += 1
242 elif _is_assistant_message(message):
243 counts["Assistant"] += 1
244 elif isinstance(message, AgentSystemMessage) or (isinstance(message, dict) and message.get("role") == "system"):
245 counts["System"] += 1
246 elif isinstance(message, dict) and message.get("type") == "function_call":
247 counts["Function Call"] += 1
248 elif isinstance(message, dict) and message.get("type") == "function_call_output":
249 counts["Function Output"] += 1
250 else:
251 counts["Unknown"] += 1
254def _is_assistant_message(message: FlexibleRunnerMessage) -> bool:
255 """判断是否为助手消息。"""
256 return isinstance(message, (AgentAssistantMessage, NewAssistantMessage)) or (isinstance(message, dict) and message.get("role") == "assistant")
259def _extract_meta_data(message: FlexibleRunnerMessage, total_input: int, total_output: int, total_latency: int, total_output_time: int) -> tuple[int, int, int, int] | None:
260 """
261 从消息中提取 meta 数据。
263 Returns:
264 更新后的统计数据元组,如果没有 meta 数据则返回 None
265 """
266 meta = None
267 if isinstance(message, NewAssistantMessage) and message.meta:
268 # Handle new message format
269 meta = message.meta
270 if meta.usage:
271 if meta.usage.input_tokens is not None:
272 total_input += meta.usage.input_tokens
273 if meta.usage.output_tokens is not None:
274 total_output += meta.usage.output_tokens
275 if meta.latency_ms is not None:
276 total_latency += meta.latency_ms
277 if meta.total_time_ms is not None:
278 total_output_time += meta.total_time_ms
279 return total_input, total_output, total_latency, total_output_time
280 if isinstance(message, AgentAssistantMessage) and message.meta:
281 meta = message.meta
282 elif isinstance(message, dict) and message.get("meta"):
283 meta = message["meta"] # type: ignore[typeddict-item]
285 if not meta:
286 return None
288 if hasattr(meta, "input_tokens"):
289 return _process_object_meta(meta, total_input, total_output, total_latency, total_output_time)
290 if isinstance(meta, dict):
291 return _process_dict_meta(meta, total_input, total_output, total_latency, total_output_time)
293 return None
296def _process_object_meta(meta: BasicMessageMeta | LLMResponseMeta | AssistantMessageMeta, total_input: int, total_output: int, total_latency: int, total_output_time: int) -> tuple[int, int, int, int]:
297 """处理对象类型的 meta 数据。"""
298 # LLMResponseMeta 和 AssistantMessageMeta 都有这些字段
299 if isinstance(meta, (LLMResponseMeta, AssistantMessageMeta)):
300 # For AssistantMessageMeta, use the structured usage field
301 if isinstance(meta, AssistantMessageMeta) and meta.usage is not None:
302 if meta.usage.input_tokens is not None:
303 total_input += int(meta.usage.input_tokens)
304 if meta.usage.output_tokens is not None:
305 total_output += int(meta.usage.output_tokens)
306 # For LLMResponseMeta, use the flat fields
307 elif isinstance(meta, LLMResponseMeta):
308 if hasattr(meta, "input_tokens") and meta.input_tokens is not None:
309 total_input += int(meta.input_tokens)
310 if hasattr(meta, "output_tokens") and meta.output_tokens is not None:
311 total_output += int(meta.output_tokens)
312 if hasattr(meta, "latency_ms") and meta.latency_ms is not None:
313 total_latency += int(meta.latency_ms)
314 if hasattr(meta, "output_time_ms") and meta.output_time_ms is not None:
315 total_output_time += int(meta.output_time_ms)
317 return total_input, total_output, total_latency, total_output_time
320def _process_dict_meta(meta: dict[str, str | int | float | None], total_input: int, total_output: int, total_latency: int, total_output_time: int) -> tuple[int, int, int, int]:
321 """处理字典类型的 meta 数据。"""
322 if meta.get("input_tokens") is not None:
323 val = meta["input_tokens"]
324 if val is not None:
325 total_input += int(val)
326 if meta.get("output_tokens") is not None:
327 val = meta["output_tokens"]
328 if val is not None:
329 total_output += int(val)
330 if meta.get("latency_ms") is not None:
331 val = meta["latency_ms"]
332 if val is not None:
333 total_latency += int(val)
334 if meta.get("output_time_ms") is not None:
335 val = meta["output_time_ms"]
336 if val is not None:
337 total_output_time += int(val)
339 return total_input, total_output, total_latency, total_output_time
342def _add_meta_stats_to_table(table: Table, meta_stats: dict[str, int | float]) -> None:
343 """添加 meta 统计信息到表格。"""
344 assistant_with_meta_count = meta_stats["assistant_with_meta_count"]
345 if assistant_with_meta_count <= 0:
346 return
348 table.add_row("", "") # 空行分隔
349 table.add_row("[bold cyan]Performance Stats[/bold cyan]", "")
351 total_input_tokens = meta_stats["total_input_tokens"]
352 total_output_tokens = meta_stats["total_output_tokens"]
353 if total_input_tokens > 0 or total_output_tokens > 0:
354 total_tokens = total_input_tokens + total_output_tokens
355 table.add_row("Total Tokens", f"↑{total_input_tokens}↓{total_output_tokens}={total_tokens}")
357 total_latency_ms = meta_stats["total_latency_ms"]
358 if assistant_with_meta_count > 0 and total_latency_ms > 0:
359 avg_latency = total_latency_ms / assistant_with_meta_count
360 table.add_row("Avg Latency", f"{avg_latency:.1f}ms")
362 total_output_time_ms = meta_stats["total_output_time_ms"]
363 if assistant_with_meta_count > 0 and total_output_time_ms > 0:
364 avg_output_time = total_output_time_ms / assistant_with_meta_count
365 table.add_row("Avg Output Time", f"{avg_output_time:.1f}ms")
368def display_chat_summary(messages: RunnerMessages, *, console: Console | None = None) -> None:
369 """
370 打印聊天记录摘要。
372 Args:
373 messages: 要汇总的消息列表
374 console: Rich Console 实例,如果为 None 则创建新的
375 """
376 active_console = console or Console()
377 summary_table = build_chat_summary_table(messages)
378 active_console.print(summary_table)
381def display_messages(
382 messages: RunnerMessages,
383 *,
384 config: DisplayConfig | None = None,
385 **kwargs: object,
386) -> None:
387 """
388 以紧凑的单行格式打印消息列表。
390 Args:
391 messages: 要打印的消息列表
392 config: 显示配置,如果为 None 则使用默认配置
393 **kwargs: 额外的配置参数,用于向后兼容
395 Example:
396 >>> from lite_agent.runner import Runner
397 >>> from lite_agent.chat_display import display_messages, DisplayConfig
398 >>>
399 >>> runner = Runner(agent=my_agent)
400 >>> # ... add some messages ...
401 >>> display_messages(runner.messages)
402 >>> # 或者使用自定义配置
403 >>> config = DisplayConfig(show_timestamps=False, max_content_length=100)
404 >>> display_messages(runner.messages, config=config)
405 """
406 if config is None:
407 # 过滤掉 None 值的 kwargs 并确保类型正确
408 filtered_kwargs = {
409 k: v
410 for k, v in kwargs.items()
411 if v is not None
412 and (
413 (k == "console" and isinstance(v, Console))
414 or (k == "show_indices" and isinstance(v, bool))
415 or (k == "show_timestamps" and isinstance(v, bool))
416 or (k == "max_content_length" and isinstance(v, int))
417 or (k == "local_timezone" and (isinstance(v, (timezone, str)) or v is None))
418 )
419 }
420 config = DisplayConfig(**filtered_kwargs) # type: ignore[arg-type]
422 console = config.console
423 if console is None:
424 console = Console()
426 if not messages:
427 console.print("[dim]No messages to display[/dim]")
428 return
430 # 处理时区参数
431 local_timezone = config.local_timezone
432 if local_timezone is None:
433 local_timezone = _get_local_timezone()
434 elif isinstance(local_timezone, str):
435 local_timezone = _get_timezone_by_name(local_timezone)
437 for i, message in enumerate(messages):
438 _display_single_message_compact(
439 message,
440 index=i if config.show_indices else None,
441 console=console,
442 max_content_length=config.max_content_length,
443 show_timestamp=config.show_timestamps,
444 local_timezone=local_timezone,
445 )
448def _display_single_message_compact(
449 message: FlexibleRunnerMessage,
450 *,
451 index: int | None = None,
452 console: Console,
453 max_content_length: int = 100,
454 show_timestamp: bool = False,
455 local_timezone: timezone | None = None,
456) -> None:
457 """以紧凑格式打印单个消息。"""
459 def truncate_content(content: str, max_length: int) -> str:
460 """截断内容并添加省略号。"""
461 if len(content) <= max_length:
462 return content
463 return content[: max_length - 3] + "..."
465 # 创建消息上下文
466 context_config = {
467 "console": console,
468 "index": index,
469 "message": message,
470 "max_content_length": max_content_length,
471 "truncate_content": truncate_content,
472 "show_timestamp": show_timestamp,
473 "local_timezone": local_timezone,
474 }
475 context = _create_message_context(context_config)
477 # 根据消息类型分发处理
478 _dispatch_message_display(message, context)
481def _create_message_context(context_config: dict[str, FlexibleRunnerMessage | Console | int | bool | timezone | Callable[[str, int], str] | None]) -> MessageContext:
482 """创建消息显示上下文。"""
483 console = context_config["console"]
484 index = context_config.get("index")
485 message = context_config["message"]
486 max_content_length_val = context_config["max_content_length"]
487 if not isinstance(max_content_length_val, int):
488 msg = "max_content_length must be an integer"
489 raise TypeError(msg)
490 max_content_length = max_content_length_val
491 truncate_content = context_config["truncate_content"]
492 show_timestamp = context_config.get("show_timestamp", False)
493 local_timezone = context_config.get("local_timezone")
495 # 类型检查
496 console_msg = "console must be a Console instance"
497 if not isinstance(console, Console):
498 raise TypeError(console_msg)
500 truncate_msg = "truncate_content must be callable"
501 if not callable(truncate_content):
502 raise TypeError(truncate_msg)
504 timezone_msg = "local_timezone must be a timezone instance"
505 if local_timezone is not None and not isinstance(local_timezone, timezone):
506 raise TypeError(timezone_msg)
508 # 获取时间戳
509 timestamp = None
510 if show_timestamp:
511 # 确保 message 是正确的类型
512 valid_types = (
513 AgentUserMessage,
514 AgentAssistantMessage,
515 AgentSystemMessage,
516 NewUserMessage,
517 NewAssistantMessage,
518 NewSystemMessage,
519 dict,
520 )
521 message_time = _extract_message_time(message) if isinstance(message, valid_types) else None
522 timestamp = _format_timestamp(message_time, local_timezone=local_timezone if isinstance(local_timezone, timezone) else None)
524 timestamp_str = f"[{timestamp}] " if timestamp else ""
525 index_str = f"#{index:2d} " if index is not None else ""
527 return MessageContext(
528 console=console,
529 index_str=index_str,
530 timestamp_str=timestamp_str,
531 max_content_length=max_content_length,
532 truncate_content=truncate_content, # type: ignore[arg-type]
533 )
536def _extract_message_time(message: FlexibleRunnerMessage | AgentUserMessage | AgentAssistantMessage | dict) -> datetime | None:
537 """从消息中提取时间戳。"""
538 # Handle new message format first
539 if (isinstance(message, NewMessage) and message.meta and message.meta.sent_at) or (isinstance(message, AgentAssistantMessage) and message.meta and message.meta.sent_at):
540 return message.meta.sent_at
541 if isinstance(message, dict) and message.get("meta") and isinstance(message["meta"], dict): # type: ignore[typeddict-item]
542 sent_at = message["meta"].get("sent_at") # type: ignore[typeddict-item]
543 if isinstance(sent_at, datetime):
544 return sent_at
545 return None
548def _dispatch_message_display(message: FlexibleRunnerMessage, context: MessageContext) -> None:
549 """根据消息类型分发显示处理。"""
550 # Handle new message format first
551 if isinstance(message, NewUserMessage):
552 _display_new_user_message_compact(message, context)
553 elif isinstance(message, NewAssistantMessage):
554 _display_new_assistant_message_compact(message, context)
555 elif isinstance(message, NewSystemMessage):
556 _display_new_system_message_compact(message, context)
557 # Handle legacy message format
558 elif isinstance(message, AgentUserMessage):
559 _display_user_message_compact_v2(message, context)
560 elif isinstance(message, AgentAssistantMessage):
561 _display_assistant_message_compact_v2(message, context)
562 elif isinstance(message, AgentSystemMessage):
563 _display_system_message_compact_v2(message, context)
564 elif isinstance(message, dict):
565 _display_dict_message_compact_v2(message, context) # type: ignore[arg-type]
566 else:
567 _display_unknown_message_compact_v2(message, context)
570def _display_user_message_compact_v2(message: AgentUserMessage, context: MessageContext) -> None:
571 """打印用户消息的紧凑格式 (v2)。"""
572 content = context.truncate_content(str(message.content), context.max_content_length)
573 context.console.print(f"{context.timestamp_str}{context.index_str}[blue]User:[/blue]\n{content}")
576def _display_assistant_message_compact_v2(message: AgentAssistantMessage, context: MessageContext) -> None:
577 """打印助手消息的紧凑格式 (v2)。"""
578 content = context.truncate_content(str(message.content), context.max_content_length)
580 # 添加 meta 数据信息(使用英文标签)
581 meta_info = ""
582 if message.meta:
583 meta_parts = []
584 if message.meta.latency_ms is not None:
585 meta_parts.append(f"Latency:{message.meta.latency_ms}ms")
586 if message.meta.output_time_ms is not None:
587 meta_parts.append(f"Output:{message.meta.output_time_ms}ms")
588 if message.meta.usage and message.meta.usage.input_tokens is not None and message.meta.usage.output_tokens is not None:
589 total_tokens = message.meta.usage.input_tokens + message.meta.usage.output_tokens
590 meta_parts.append(f"Tokens:↑{message.meta.usage.input_tokens}↓{message.meta.usage.output_tokens}={total_tokens}")
592 if meta_parts:
593 meta_info = f" [dim]({' | '.join(meta_parts)})[/dim]"
595 context.console.print(f"{context.timestamp_str}{context.index_str}[green]Assistant:[/green]{meta_info}\n{content}")
598def _display_system_message_compact_v2(message: AgentSystemMessage, context: MessageContext) -> None:
599 """打印系统消息的紧凑格式 (v2)。"""
600 content = context.truncate_content(str(message.content), context.max_content_length)
601 context.console.print(f"{context.timestamp_str}{context.index_str}[yellow]System:[/yellow]\n{content}")
604def _display_unknown_message_compact_v2(message: FlexibleRunnerMessage, context: MessageContext) -> None:
605 """打印未知类型消息的紧凑格式 (v2)。"""
606 try:
607 content = str(message.model_dump()) if hasattr(message, "model_dump") else str(message) # type: ignore[attr-defined]
608 except Exception:
609 content = str(message)
611 content = context.truncate_content(content, context.max_content_length)
612 context.console.print(f"{context.timestamp_str}{context.index_str}[red]Unknown:[/red]\n{content}")
615def _display_dict_message_compact_v2(message: dict, context: MessageContext) -> None:
616 """以紧凑格式打印字典消息 (v2)。"""
617 message_type = message.get("type")
618 role = message.get("role")
620 if message_type == "function_call":
621 _display_dict_function_call_compact(message, context)
622 elif message_type == "function_call_output":
623 _display_dict_function_output_compact(message, context)
624 elif role == "user":
625 _display_dict_user_compact(message, context)
626 elif role == "assistant":
627 _display_dict_assistant_compact(message, context)
628 elif role == "system":
629 _display_dict_system_compact(message, context)
630 else:
631 # 未知类型的字典消息
632 content = context.truncate_content(str(message), context.max_content_length)
633 context.console.print(f"{context.timestamp_str}{context.index_str}[red]Unknown:[/red]")
634 context.console.print(f" {content}")
637def _display_dict_function_call_compact(message: dict, context: MessageContext) -> None:
638 """显示字典类型的函数调用消息。"""
639 name = str(message.get("name", "unknown"))
640 args = str(message.get("arguments", ""))
642 args_str = ""
643 if args:
644 try:
645 parsed_args = json.loads(args)
646 args_str = f" {parsed_args}"
647 except (json.JSONDecodeError, TypeError):
648 args_str = f" {args}"
650 args_display = context.truncate_content(args_str, context.max_content_length - len(name) - 10)
651 context.console.print(f"{context.timestamp_str}{context.index_str}[magenta]Call:[/magenta] {name}")
652 if args_display.strip(): # Only show args if they exist
653 context.console.print(f"{args_display.strip()}")
656def _display_dict_function_output_compact(message: dict, context: MessageContext) -> None:
657 """显示字典类型的函数输出消息。"""
658 output = context.truncate_content(str(message.get("output", "")), context.max_content_length)
659 context.console.print(f"{context.timestamp_str}{context.index_str}[cyan]Output:[/cyan]")
660 context.console.print(f"{output}")
663def _display_dict_user_compact(message: dict, context: MessageContext) -> None:
664 """显示字典类型的用户消息。"""
665 content = context.truncate_content(str(message.get("content", "")), context.max_content_length)
666 context.console.print(f"{context.timestamp_str}{context.index_str}[blue]User:[/blue]")
667 context.console.print(f"{content}")
670def _display_dict_assistant_compact(message: dict, context: MessageContext) -> None:
671 """显示字典类型的助手消息。"""
672 content = context.truncate_content(str(message.get("content", "")), context.max_content_length)
674 # 添加 meta 数据信息(使用英文标签)
675 meta_info = ""
676 meta = message.get("meta")
677 if meta and isinstance(meta, dict):
678 meta_parts = []
679 if meta.get("latency_ms") is not None:
680 meta_parts.append(f"Latency:{meta['latency_ms']}ms")
681 if meta.get("output_time_ms") is not None:
682 meta_parts.append(f"Output:{meta['output_time_ms']}ms")
683 if meta.get("input_tokens") is not None and meta.get("output_tokens") is not None:
684 total_tokens = meta["input_tokens"] + meta["output_tokens"]
685 meta_parts.append(f"Tokens:↑{meta['input_tokens']}↓{meta['output_tokens']}={total_tokens}")
687 if meta_parts:
688 meta_info = f" [dim]({' | '.join(meta_parts)})[/dim]"
690 context.console.print(f"{context.timestamp_str}{context.index_str}[green]Assistant:[/green]{meta_info}")
691 context.console.print(f"{content}")
694def _display_dict_system_compact(message: dict, context: MessageContext) -> None:
695 """显示字典类型的系统消息。"""
696 content = context.truncate_content(str(message.get("content", "")), context.max_content_length)
697 context.console.print(f"{context.timestamp_str}{context.index_str}[yellow]System:[/yellow]")
698 context.console.print(f"{content}")
701# New message format display functions
702def _display_new_user_message_compact(message: NewUserMessage, context: MessageContext) -> None:
703 """显示新格式用户消息的紧凑格式。"""
704 # Combine all content into a single string
705 content_parts = []
706 for item in message.content:
707 if item.type == "text":
708 content_parts.append(item.text)
709 elif item.type == "image":
710 if item.image_url:
711 content_parts.append(f"[Image: {item.image_url}]")
712 elif item.file_id:
713 content_parts.append(f"[Image: {item.file_id}]")
714 elif item.type == "file":
715 file_name = item.file_name or item.file_id
716 content_parts.append(f"[File: {file_name}]")
718 content = " ".join(content_parts)
719 content = context.truncate_content(content, context.max_content_length)
720 context.console.print(f"{context.timestamp_str}{context.index_str}[blue]User:[/blue]")
721 context.console.print(f"{content}")
724def _display_new_system_message_compact(message: NewSystemMessage, context: MessageContext) -> None:
725 """显示新格式系统消息的紧凑格式。"""
726 content = context.truncate_content(message.content, context.max_content_length)
727 context.console.print(f"{context.timestamp_str}{context.index_str}[yellow]System:[/yellow]")
728 context.console.print(f"{content}")
731def _display_new_assistant_message_compact(message: NewAssistantMessage, context: MessageContext) -> None:
732 """显示新格式助手消息的紧凑格式。"""
733 # Extract text content and tool information
734 text_parts = []
735 tool_calls = []
736 tool_results = []
738 for item in message.content:
739 if item.type == "text":
740 text_parts.append(item.text)
741 elif item.type == "tool_call":
742 tool_calls.append(item)
743 elif item.type == "tool_call_result":
744 tool_results.append(item)
746 # Display text content first if available
747 if text_parts:
748 content = " ".join(text_parts)
749 content = context.truncate_content(content, context.max_content_length)
751 # Add meta data information (使用英文标签)
752 meta_info = ""
753 if message.meta:
754 meta_parts = []
755 if message.meta.latency_ms is not None:
756 meta_parts.append(f"Latency:{message.meta.latency_ms}ms")
757 if message.meta.total_time_ms is not None:
758 meta_parts.append(f"Output:{message.meta.total_time_ms}ms")
759 if message.meta.usage and message.meta.usage.input_tokens is not None and message.meta.usage.output_tokens is not None:
760 total_tokens = message.meta.usage.input_tokens + message.meta.usage.output_tokens
761 meta_parts.append(f"Tokens:↑{message.meta.usage.input_tokens}↓{message.meta.usage.output_tokens}={total_tokens}")
763 if meta_parts:
764 meta_info = f" [dim]({' | '.join(meta_parts)})[/dim]"
766 context.console.print(f"{context.timestamp_str}{context.index_str}[green]Assistant:[/green]{meta_info}")
767 context.console.print(f"{content}")
769 # Display tool calls
770 for tool_call in tool_calls:
771 args_str = ""
772 if tool_call.arguments:
773 try:
774 parsed_args = json.loads(tool_call.arguments) if isinstance(tool_call.arguments, str) else tool_call.arguments
775 args_str = f" {parsed_args}"
776 except (json.JSONDecodeError, TypeError):
777 args_str = f" {tool_call.arguments}"
779 args_display = context.truncate_content(args_str, context.max_content_length - len(tool_call.name) - 10)
780 context.console.print(f"{context.timestamp_str}{context.index_str}[magenta]Call:[/magenta]")
781 context.console.print(f"{tool_call.name}{args_display}")
783 # Display tool results
784 for tool_result in tool_results:
785 output = context.truncate_content(str(tool_result.output), context.max_content_length)
786 context.console.print(f"{context.timestamp_str}{context.index_str}[cyan]Output:[/cyan]")
787 context.console.print(f"{output}")