lite-agent 0.3.0__py3-none-any.whl → 0.4.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of lite-agent might be problematic. Click here for more details.

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