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

1""" 

2Chat display utilities for lite-agent. 

3 

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""" 

8 

9import json 

10import time 

11from collections.abc import Callable 

12from datetime import datetime, timedelta, timezone 

13 

14try: 

15 from zoneinfo import ZoneInfo 

16except ImportError: 

17 ZoneInfo = None 

18 

19from dataclasses import dataclass 

20 

21from rich.console import Console 

22from rich.table import Table 

23 

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) 

40 

41 

42@dataclass 

43class DisplayConfig: 

44 """消息显示配置。""" 

45 

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 

51 

52 

53@dataclass 

54class MessageContext: 

55 """消息显示上下文。""" 

56 

57 console: Console 

58 index_str: str 

59 timestamp_str: str 

60 max_content_length: int 

61 truncate_content: Callable[[str, int], str] 

62 

63 

64def _get_local_timezone() -> timezone: 

65 """ 

66 检测并返回用户本地时区。 

67 

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)) 

75 

76 

77def _get_timezone_by_name(timezone_name: str) -> timezone: # noqa: PLR0911 

78 """ 

79 根据时区名称获取时区对象。 

80 

81 Args: 

82 timezone_name: 时区名称,支持: 

83 - "local": 自动检测本地时区 

84 - "UTC": UTC 时区 

85 - "+8", "-5": UTC 偏移量(小时) 

86 - "Asia/Shanghai", "America/New_York": IANA 时区名称(需要 zoneinfo) 

87 

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() 

113 

114 

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 格式化时间戳,自动转换为本地时区。 

123 

124 Args: 

125 dt: 要格式化的 datetime 对象,如果为 None 则使用当前时间 

126 local_timezone: 本地时区,如果为 None 则自动检测 

127 format_str: 时间格式字符串 

128 

129 Returns: 

130 格式化后的时间字符串 

131 """ 

132 if dt is None: 

133 dt = datetime.now(timezone.utc) 

134 

135 if local_timezone is None: 

136 local_timezone = _get_local_timezone() 

137 

138 # 如果 datetime 对象没有时区信息,假设为 UTC 

139 if dt.tzinfo is None: 

140 dt = dt.replace(tzinfo=timezone.utc) 

141 

142 # 转换到本地时区 

143 local_dt = dt.astimezone(local_timezone) 

144 return local_dt.strftime(format_str) 

145 

146 

147def build_chat_summary_table(messages: RunnerMessages) -> Table: 

148 """ 

149 创建聊天记录摘要表格。 

150 

151 Args: 

152 messages: 要汇总的消息列表 

153 

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") 

160 

161 # 统计各种消息类型和 meta 数据 

162 counts, meta_stats = _analyze_messages(messages) 

163 

164 # 只显示计数大于0的类型 

165 for msg_type, count in counts.items(): 

166 if count > 0: 

167 table.add_row(msg_type, str(count)) 

168 

169 table.add_row("[bold]Total[/bold]", f"[bold]{len(messages)}[/bold]") 

170 

171 # 添加 meta 数据统计 

172 _add_meta_stats_to_table(table, meta_stats) 

173 

174 return table 

175 

176 

177def _analyze_messages(messages: RunnerMessages) -> tuple[dict[str, int], dict[str, int | float]]: 

178 """ 

179 分析消息并返回统计信息。 

180 

181 Args: 

182 messages: 要分析的消息列表 

183 

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 } 

195 

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 

202 

203 for message in messages: 

204 _update_message_counts(message, counts) 

205 

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 

212 

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 

222 

223 

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 

252 

253 

254def _is_assistant_message(message: FlexibleRunnerMessage) -> bool: 

255 """判断是否为助手消息。""" 

256 return isinstance(message, (AgentAssistantMessage, NewAssistantMessage)) or (isinstance(message, dict) and message.get("role") == "assistant") 

257 

258 

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 数据。 

262 

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] 

284 

285 if not meta: 

286 return None 

287 

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) 

292 

293 return None 

294 

295 

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) 

316 

317 return total_input, total_output, total_latency, total_output_time 

318 

319 

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) 

338 

339 return total_input, total_output, total_latency, total_output_time 

340 

341 

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 

347 

348 table.add_row("", "") # 空行分隔 

349 table.add_row("[bold cyan]Performance Stats[/bold cyan]", "") 

350 

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}") 

356 

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") 

361 

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") 

366 

367 

368def display_chat_summary(messages: RunnerMessages, *, console: Console | None = None) -> None: 

369 """ 

370 打印聊天记录摘要。 

371 

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) 

379 

380 

381def display_messages( 

382 messages: RunnerMessages, 

383 *, 

384 config: DisplayConfig | None = None, 

385 **kwargs: object, 

386) -> None: 

387 """ 

388 以紧凑的单行格式打印消息列表。 

389 

390 Args: 

391 messages: 要打印的消息列表 

392 config: 显示配置,如果为 None 则使用默认配置 

393 **kwargs: 额外的配置参数,用于向后兼容 

394 

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] 

421 

422 console = config.console 

423 if console is None: 

424 console = Console() 

425 

426 if not messages: 

427 console.print("[dim]No messages to display[/dim]") 

428 return 

429 

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) 

436 

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 ) 

446 

447 

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 """以紧凑格式打印单个消息。""" 

458 

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] + "..." 

464 

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) 

476 

477 # 根据消息类型分发处理 

478 _dispatch_message_display(message, context) 

479 

480 

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") 

494 

495 # 类型检查 

496 console_msg = "console must be a Console instance" 

497 if not isinstance(console, Console): 

498 raise TypeError(console_msg) 

499 

500 truncate_msg = "truncate_content must be callable" 

501 if not callable(truncate_content): 

502 raise TypeError(truncate_msg) 

503 

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) 

507 

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) 

523 

524 timestamp_str = f"[{timestamp}] " if timestamp else "" 

525 index_str = f"#{index:2d} " if index is not None else "" 

526 

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 ) 

534 

535 

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 

546 

547 

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) 

568 

569 

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}") 

574 

575 

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) 

579 

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}") 

591 

592 if meta_parts: 

593 meta_info = f" [dim]({' | '.join(meta_parts)})[/dim]" 

594 

595 context.console.print(f"{context.timestamp_str}{context.index_str}[green]Assistant:[/green]{meta_info}\n{content}") 

596 

597 

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}") 

602 

603 

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) 

610 

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}") 

613 

614 

615def _display_dict_message_compact_v2(message: dict, context: MessageContext) -> None: 

616 """以紧凑格式打印字典消息 (v2)。""" 

617 message_type = message.get("type") 

618 role = message.get("role") 

619 

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}") 

635 

636 

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", "")) 

641 

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}" 

649 

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()}") 

654 

655 

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}") 

661 

662 

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}") 

668 

669 

670def _display_dict_assistant_compact(message: dict, context: MessageContext) -> None: 

671 """显示字典类型的助手消息。""" 

672 content = context.truncate_content(str(message.get("content", "")), context.max_content_length) 

673 

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}") 

686 

687 if meta_parts: 

688 meta_info = f" [dim]({' | '.join(meta_parts)})[/dim]" 

689 

690 context.console.print(f"{context.timestamp_str}{context.index_str}[green]Assistant:[/green]{meta_info}") 

691 context.console.print(f"{content}") 

692 

693 

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}") 

699 

700 

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}]") 

717 

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}") 

722 

723 

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}") 

729 

730 

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 = [] 

737 

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) 

745 

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) 

750 

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}") 

762 

763 if meta_parts: 

764 meta_info = f" [dim]({' | '.join(meta_parts)})[/dim]" 

765 

766 context.console.print(f"{context.timestamp_str}{context.index_str}[green]Assistant:[/green]{meta_info}") 

767 context.console.print(f"{content}") 

768 

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}" 

778 

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}") 

782 

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}")