lite-agent 0.3.0__py3-none-any.whl → 0.4.0__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.
- lite_agent/__init__.py +2 -2
- lite_agent/agent.py +178 -68
- lite_agent/chat_display.py +779 -0
- lite_agent/client.py +36 -1
- lite_agent/message_transfers.py +9 -1
- lite_agent/processors/__init__.py +3 -2
- lite_agent/processors/completion_event_processor.py +306 -0
- lite_agent/processors/response_event_processor.py +205 -0
- lite_agent/runner.py +413 -230
- lite_agent/stream_handlers/__init__.py +3 -2
- lite_agent/stream_handlers/litellm.py +37 -68
- lite_agent/types/__init__.py +77 -23
- lite_agent/types/events.py +119 -0
- lite_agent/types/messages.py +256 -48
- {lite_agent-0.3.0.dist-info → lite_agent-0.4.0.dist-info}/METADATA +2 -2
- lite_agent-0.4.0.dist-info/RECORD +23 -0
- lite_agent/processors/stream_chunk_processor.py +0 -106
- lite_agent/rich_helpers.py +0 -503
- lite_agent/types/chunks.py +0 -89
- lite_agent-0.3.0.dist-info/RECORD +0 -22
- {lite_agent-0.3.0.dist-info → lite_agent-0.4.0.dist-info}/WHEEL +0 -0
|
@@ -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}")
|