lite-agent 0.1.0__py3-none-any.whl → 0.3.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.

@@ -1,53 +1,61 @@
1
1
  import litellm
2
- from funcall import Funcall
3
- from litellm.types.utils import ChatCompletionDeltaToolCall, StreamingChoices
2
+ from litellm.types.utils import ChatCompletionDeltaToolCall, ModelResponseStream, StreamingChoices
4
3
 
5
- from open_agents.loggers import logger
6
- from open_agents.types import AssistantMessage, ToolCall, ToolCallFunction
4
+ from lite_agent.loggers import logger
5
+ from lite_agent.types import AssistantMessage, ToolCall, ToolCallFunction
7
6
 
8
7
 
9
8
  class StreamChunkProcessor:
10
9
  """Processor for handling streaming responses"""
11
10
 
12
- def __init__(self, fc: Funcall) -> None:
13
- self.fc = fc
14
- self.current_message: AssistantMessage = None
11
+ def __init__(self) -> None:
12
+ self._current_message: AssistantMessage | None = None
15
13
 
16
- def initialize_message(self, chunk: litellm.ModelResponseStream, choice: StreamingChoices) -> None:
14
+ def initialize_message(self, chunk: ModelResponseStream, choice: StreamingChoices) -> None:
17
15
  """Initialize the message object"""
18
16
  delta = choice.delta
19
- self.current_message = AssistantMessage(
17
+ if delta.role != "assistant":
18
+ logger.warning("Skipping chunk with role: %s", delta.role)
19
+ return
20
+ self._current_message = AssistantMessage(
20
21
  id=chunk.id,
21
22
  index=choice.index,
22
23
  role=delta.role,
23
24
  content="",
24
25
  )
25
- logger.debug("Initialized new message: %s", self.current_message.id)
26
+ logger.debug('Initialized new message: "%s"', self._current_message.id)
26
27
 
27
28
  def update_content(self, content: str) -> None:
28
29
  """Update message content"""
29
- if self.current_message and content:
30
- self.current_message.content += content
30
+ if self._current_message and content:
31
+ self._current_message.content += content
31
32
 
32
33
  def _initialize_tool_calls(self, tool_calls: list[litellm.ChatCompletionMessageToolCall]) -> None:
33
34
  """Initialize tool calls"""
34
- if not self.current_message:
35
+ if not self._current_message:
35
36
  return
36
37
 
37
- self.current_message.tool_calls = []
38
+ self._current_message.tool_calls = []
38
39
  for call in tool_calls:
39
40
  logger.debug("Create new tool call: %s", call.id)
40
41
 
41
42
  def _update_tool_calls(self, tool_calls: list[litellm.ChatCompletionMessageToolCall]) -> None:
42
43
  """Update existing tool calls"""
43
- if not self.current_message or not self.current_message.tool_calls:
44
+ if not self._current_message:
44
45
  return
45
-
46
- for current_call, new_call in zip(self.current_message.tool_calls, tool_calls, strict=False):
47
- if new_call.function.arguments:
46
+ if not hasattr(self._current_message, "tool_calls"):
47
+ self._current_message.tool_calls = []
48
+ if not self._current_message.tool_calls:
49
+ return
50
+ if not tool_calls:
51
+ return
52
+ for current_call, new_call in zip(self._current_message.tool_calls, tool_calls, strict=False):
53
+ if new_call.function.arguments and current_call.function.arguments:
48
54
  current_call.function.arguments += new_call.function.arguments
49
- if new_call.type:
55
+ if new_call.type and new_call.type == "function":
50
56
  current_call.type = new_call.type
57
+ elif new_call.type:
58
+ logger.warning("Unexpected tool call type: %s", new_call.type)
51
59
 
52
60
  def update_tool_calls(self, tool_calls: list[ChatCompletionDeltaToolCall]) -> None:
53
61
  """Handle tool call updates"""
@@ -55,31 +63,44 @@ class StreamChunkProcessor:
55
63
  return
56
64
  for call in tool_calls:
57
65
  if call.id:
58
- new_tool_call = ToolCall(
59
- id=call.id,
60
- type=call.type,
61
- function=ToolCallFunction(
62
- name=call.function.name or "",
63
- arguments=call.function.arguments,
64
- ),
65
- index=call.index,
66
- )
67
- if self.current_message.tool_calls is None:
68
- self.current_message.tool_calls = []
69
- self.current_message.tool_calls.append(new_tool_call)
70
- else:
71
- existing_call = self.current_message.tool_calls[call.index]
66
+ if call.type == "function":
67
+ new_tool_call = ToolCall(
68
+ id=call.id,
69
+ type=call.type,
70
+ function=ToolCallFunction(
71
+ name=call.function.name or "",
72
+ arguments=call.function.arguments,
73
+ ),
74
+ index=call.index,
75
+ )
76
+ if self._current_message is not None:
77
+ if self._current_message.tool_calls is None:
78
+ self._current_message.tool_calls = []
79
+ self._current_message.tool_calls.append(new_tool_call)
80
+ else:
81
+ logger.warning("Unexpected tool call type: %s", call.type)
82
+ elif self._current_message is not None and self._current_message.tool_calls is not None and call.index is not None and 0 <= call.index < len(self._current_message.tool_calls):
83
+ existing_call = self._current_message.tool_calls[call.index]
72
84
  if call.function.arguments:
85
+ if existing_call.function.arguments is None:
86
+ existing_call.function.arguments = ""
73
87
  existing_call.function.arguments += call.function.arguments
88
+ else:
89
+ logger.warning("Cannot update tool call: current_message or tool_calls is None, or invalid index.")
74
90
 
75
- def handle_usage_info(self, chunk: litellm.ModelResponseStream) -> litellm.Usage | None:
91
+ def handle_usage_info(self, chunk: ModelResponseStream) -> litellm.Usage | None:
76
92
  """Handle usage info, return whether this chunk should be skipped"""
77
- usage = getattr(chunk, "usage", None)
78
- if usage:
79
- logger.debug("Model usage: %s", usage)
80
- return usage
93
+ return getattr(chunk, "usage", None)
94
+
95
+ @property
96
+ def is_initialized(self) -> bool:
97
+ """Check if the current message is initialized"""
98
+ return self._current_message is not None
81
99
 
82
- def finalize_message(self) -> AssistantMessage:
83
- """Finalize message processing"""
84
- logger.debug("Message finalized: %s", self.current_message)
85
- return self.current_message
100
+ @property
101
+ def current_message(self) -> AssistantMessage:
102
+ """Get the current message being processed"""
103
+ if not self._current_message:
104
+ msg = "No current message initialized. Call initialize_message first."
105
+ raise ValueError(msg)
106
+ return self._current_message
@@ -0,0 +1,503 @@
1
+ """
2
+ Rich chat history renderer for lite-agent.
3
+
4
+ This module provides utilities to beautifully render 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
+ from datetime import datetime, timezone
11
+
12
+ from rich.console import Console
13
+ from rich.panel import Panel
14
+ from rich.syntax import Syntax
15
+ from rich.table import Table
16
+
17
+ from lite_agent.types import (
18
+ AgentAssistantMessage,
19
+ AgentFunctionCallOutput,
20
+ AgentFunctionToolCallMessage,
21
+ AgentSystemMessage,
22
+ AgentUserMessage,
23
+ RunnerMessages,
24
+ )
25
+
26
+
27
+ def print_chat_history(
28
+ messages: RunnerMessages,
29
+ *,
30
+ console: Console | None = None,
31
+ show_timestamps: bool = True,
32
+ show_indices: bool = True,
33
+ chat_width: int = 80,
34
+ ) -> None:
35
+ """
36
+ 使用 rich 库美观地渲染聊天记录。
37
+
38
+ Args:
39
+ messages: 要渲染的消息列表
40
+ console: Rich Console 实例,如果为 None 则创建新的
41
+ show_timestamps: 是否显示时间戳
42
+ show_indices: 是否显示消息索引
43
+ chat_width: 聊天气泡的最大宽度
44
+
45
+ Example:
46
+ >>> from lite_agent.runner import Runner
47
+ >>> from lite_agent.rich_helpers import render_chat_history
48
+ >>>
49
+ >>> runner = Runner(agent=my_agent)
50
+ >>> # ... add some messages ...
51
+ >>> render_chat_history(runner.messages)
52
+ """
53
+ if console is None:
54
+ console = Console()
55
+
56
+ if not messages:
57
+ console.print("[dim]No messages to display[/dim]")
58
+ return
59
+
60
+ console.print(f"\n[bold blue]Chat History[/bold blue] ([dim]{len(messages)} messages[/dim])\n")
61
+
62
+ for i, message in enumerate(messages):
63
+ _render_single_message(
64
+ message,
65
+ index=i if show_indices else None,
66
+ console=console,
67
+ show_timestamp=show_timestamps,
68
+ chat_width=chat_width,
69
+ )
70
+
71
+
72
+ def _render_single_message(
73
+ message: object,
74
+ *,
75
+ index: int | None = None,
76
+ console: Console,
77
+ show_timestamp: bool = True,
78
+ chat_width: int = 80,
79
+ ) -> None:
80
+ """渲染单个消息。"""
81
+ timestamp = datetime.now(timezone.utc).strftime("%H:%M:%S") if show_timestamp else None
82
+
83
+ # 处理不同类型的消息
84
+ if isinstance(message, AgentUserMessage):
85
+ _render_user_message(message, index, console, timestamp, chat_width)
86
+ elif isinstance(message, AgentAssistantMessage):
87
+ _render_assistant_message(message, index, console, timestamp, chat_width)
88
+ elif isinstance(message, AgentSystemMessage):
89
+ _render_system_message(message, index, console, timestamp, chat_width)
90
+ elif isinstance(message, AgentFunctionToolCallMessage):
91
+ _render_function_call_message(message, index, console, timestamp, chat_width)
92
+ elif isinstance(message, AgentFunctionCallOutput):
93
+ _render_function_output_message(message, index, console, timestamp, chat_width)
94
+ elif isinstance(message, dict):
95
+ _render_dict_message(message, index, console, timestamp, chat_width)
96
+ else:
97
+ _render_unknown_message(message, index, console, timestamp, chat_width)
98
+
99
+
100
+ def _render_user_message(
101
+ message: AgentUserMessage,
102
+ index: int | None,
103
+ console: Console,
104
+ timestamp: str | None,
105
+ chat_width: int,
106
+ ) -> None:
107
+ """渲染用户消息 - 靠右显示的蓝色气泡。"""
108
+ content = str(message.content) # 显示完整内容,不截断
109
+
110
+ title_parts = ["👤 User"]
111
+ if index is not None:
112
+ title_parts.append(f"#{index}")
113
+ if timestamp:
114
+ title_parts.append(f"[dim]{timestamp}[/dim]")
115
+
116
+ title = " ".join(title_parts)
117
+
118
+ # 计算内容的实际宽度,用于气泡大小
119
+ content_width = min(len(content) + 4, chat_width) # +4 for padding
120
+ bubble_width = max(content_width, 20) # 最小宽度
121
+
122
+ # 创建用户消息气泡 - 靠右
123
+ panel = Panel(
124
+ content,
125
+ title=title,
126
+ title_align="left",
127
+ border_style="blue",
128
+ padding=(0, 1),
129
+ width=bubble_width,
130
+ )
131
+
132
+ # 用户消息靠右
133
+ console.print(panel, justify="right")
134
+
135
+
136
+ def _render_assistant_message(
137
+ message: AgentAssistantMessage,
138
+ index: int | None,
139
+ console: Console,
140
+ timestamp: str | None,
141
+ chat_width: int,
142
+ ) -> None:
143
+ """渲染助手消息 - 靠左显示的绿色气泡。"""
144
+ content = message.content # 显示完整内容,不截断
145
+
146
+ title_parts = ["🤖 Assistant"]
147
+ if index is not None:
148
+ title_parts.append(f"#{index}")
149
+ if timestamp:
150
+ title_parts.append(f"[dim]{timestamp}[/dim]")
151
+
152
+ title = " ".join(title_parts)
153
+
154
+ # 计算内容的实际宽度,用于气泡大小
155
+ content_width = min(len(content) + 4, chat_width) # +4 for padding
156
+ bubble_width = max(content_width, 20) # 最小宽度
157
+
158
+ # 创建助手消息气泡 - 靠左
159
+ panel = Panel(
160
+ content,
161
+ title=title,
162
+ title_align="left",
163
+ border_style="green",
164
+ padding=(0, 1),
165
+ width=bubble_width,
166
+ )
167
+
168
+ # 助手消息靠左
169
+ console.print(panel)
170
+
171
+
172
+ def _render_system_message(
173
+ message: AgentSystemMessage,
174
+ index: int | None,
175
+ console: Console,
176
+ timestamp: str | None,
177
+ chat_width: int,
178
+ ) -> None:
179
+ """渲染系统消息 - 居中显示的黄色气泡。"""
180
+ content = message.content # 显示完整内容,不截断
181
+
182
+ title_parts = ["⚙️ System"]
183
+ if index is not None:
184
+ title_parts.append(f"#{index}")
185
+ if timestamp:
186
+ title_parts.append(f"[dim]{timestamp}[/dim]")
187
+
188
+ title = " ".join(title_parts)
189
+
190
+ # 系统消息居中显示,使用较小的宽度
191
+ console.print(
192
+ Panel(
193
+ content,
194
+ title=title,
195
+ title_align="center",
196
+ border_style="yellow",
197
+ padding=(0, 1),
198
+ width=min(len(content) + 10, chat_width),
199
+ ),
200
+ justify="center",
201
+ )
202
+
203
+
204
+ def _render_function_call_message(
205
+ message: AgentFunctionToolCallMessage,
206
+ index: int | None,
207
+ console: Console,
208
+ timestamp: str | None,
209
+ chat_width: int,
210
+ ) -> None:
211
+ """渲染函数调用消息 - 靠左显示的紫色气泡。"""
212
+ title_parts = ["🛠️ Function Call"]
213
+ if index is not None:
214
+ title_parts.append(f"#{index}")
215
+ if timestamp:
216
+ title_parts.append(f"[dim]{timestamp}[/dim]")
217
+
218
+ title = " ".join(title_parts)
219
+
220
+ # 创建表格显示函数调用详情
221
+ table = Table(show_header=False, box=None, padding=0)
222
+ table.add_column("Field", style="cyan", width=12)
223
+ table.add_column("Value", style="white")
224
+
225
+ table.add_row("Name:", f"[bold]{message.name}[/bold]")
226
+ table.add_row("Call ID:", f"[dim]{message.function_call_id}[/dim]")
227
+
228
+ if message.arguments:
229
+ # 尝试格式化 JSON 参数 - 显示完整内容
230
+ try:
231
+ parsed_args = json.loads(message.arguments)
232
+ formatted_args = json.dumps(parsed_args, indent=2, ensure_ascii=False)
233
+ syntax = Syntax(formatted_args, "json", theme="monokai", line_numbers=False)
234
+ table.add_row("Arguments:", syntax)
235
+ except (json.JSONDecodeError, TypeError):
236
+ table.add_row("Arguments:", message.arguments)
237
+
238
+ # 函数调用消息靠左
239
+ console.print(
240
+ Panel(
241
+ table,
242
+ title=title,
243
+ title_align="left",
244
+ border_style="magenta",
245
+ padding=(0, 1),
246
+ width=min(chat_width, 100),
247
+ ),
248
+ )
249
+
250
+
251
+ def _render_function_output_message(
252
+ message: AgentFunctionCallOutput,
253
+ index: int | None,
254
+ console: Console,
255
+ timestamp: str | None,
256
+ chat_width: int,
257
+ ) -> None:
258
+ """渲染函数输出消息 - 靠左显示的青色气泡。"""
259
+ title_parts = ["📤 Function Output"]
260
+ if index is not None:
261
+ title_parts.append(f"#{index}")
262
+ if timestamp:
263
+ title_parts.append(f"[dim]{timestamp}[/dim]")
264
+
265
+ title = " ".join(title_parts)
266
+
267
+ output_content = message.output # 显示完整内容,不截断
268
+
269
+ # 创建表格显示函数输出详情
270
+ table = Table(show_header=False, box=None, padding=0)
271
+ table.add_column("Field", style="cyan", width=12)
272
+ table.add_column("Value", style="white")
273
+
274
+ table.add_row("Call ID:", f"[dim]{message.call_id}[/dim]")
275
+ table.add_row("Output:", output_content)
276
+
277
+ # 函数输出消息靠左
278
+ console.print(
279
+ Panel(
280
+ table,
281
+ title=title,
282
+ title_align="left",
283
+ border_style="cyan",
284
+ padding=(0, 1),
285
+ width=min(chat_width, 100),
286
+ ),
287
+ )
288
+
289
+
290
+ def _render_role_based_dict_message( # noqa: PLR0913
291
+ *,
292
+ message: dict[str, object],
293
+ role: str,
294
+ index: int | None,
295
+ console: Console,
296
+ timestamp: str | None,
297
+ chat_width: int,
298
+ ) -> None:
299
+ """渲染基于角色的字典消息。"""
300
+ content = str(message.get("content", "")) # 显示完整内容,不截断
301
+
302
+ title_parts = []
303
+ if role == "user":
304
+ title_parts = ["👤 User"]
305
+ border_style = "blue"
306
+ # 用户消息靠右
307
+ content_width = min(len(content) + 4, chat_width)
308
+ bubble_width = max(content_width, 20)
309
+ if index is not None:
310
+ title_parts.append(f"#{index}")
311
+ if timestamp:
312
+ title_parts.append(f"[dim]{timestamp}[/dim]")
313
+
314
+ panel = Panel(
315
+ content,
316
+ title=" ".join(title_parts),
317
+ title_align="left",
318
+ border_style=border_style,
319
+ padding=(0, 1),
320
+ width=bubble_width,
321
+ )
322
+ console.print(panel, justify="right")
323
+ elif role == "assistant":
324
+ title_parts = ["🤖 Assistant"]
325
+ border_style = "green"
326
+ # 助手消息靠左
327
+ content_width = min(len(content) + 4, chat_width)
328
+ bubble_width = max(content_width, 20)
329
+ if index is not None:
330
+ title_parts.append(f"#{index}")
331
+ if timestamp:
332
+ title_parts.append(f"[dim]{timestamp}[/dim]")
333
+
334
+ panel = Panel(
335
+ content,
336
+ title=" ".join(title_parts),
337
+ title_align="left",
338
+ border_style=border_style,
339
+ padding=(0, 1),
340
+ width=bubble_width,
341
+ )
342
+ # 助手消息靠左
343
+ console.print(panel)
344
+ else: # system
345
+ title_parts = ["⚙️ System"]
346
+ border_style = "yellow"
347
+ if index is not None:
348
+ title_parts.append(f"#{index}")
349
+ if timestamp:
350
+ title_parts.append(f"[dim]{timestamp}[/dim]")
351
+
352
+ # 系统消息居中
353
+ console.print(
354
+ Panel(
355
+ content,
356
+ title=" ".join(title_parts),
357
+ title_align="center",
358
+ border_style=border_style,
359
+ padding=(0, 1),
360
+ width=min(len(content) + 10, chat_width),
361
+ ),
362
+ justify="center",
363
+ )
364
+
365
+
366
+ def _render_dict_message(
367
+ message: dict[str, object],
368
+ index: int | None,
369
+ console: Console,
370
+ timestamp: str | None,
371
+ chat_width: int,
372
+ ) -> None:
373
+ """渲染字典格式的消息。"""
374
+ message_type = message.get("type")
375
+ role = message.get("role")
376
+
377
+ if message_type == "function_call":
378
+ # 创建临时 AgentFunctionToolCallMessage 对象进行渲染
379
+ temp_message = AgentFunctionToolCallMessage(
380
+ type="function_call",
381
+ function_call_id=str(message.get("function_call_id", "")),
382
+ name=str(message.get("name", "unknown")),
383
+ arguments=str(message.get("arguments", "")),
384
+ content=str(message.get("content", "")),
385
+ )
386
+ _render_function_call_message(temp_message, index, console, timestamp, chat_width)
387
+ elif message_type == "function_call_output":
388
+ # 创建临时 AgentFunctionCallOutput 对象进行渲染
389
+ temp_message = AgentFunctionCallOutput(
390
+ type="function_call_output",
391
+ call_id=str(message.get("call_id", "")),
392
+ output=str(message.get("output", "")),
393
+ )
394
+ _render_function_output_message(temp_message, index, console, timestamp, chat_width)
395
+ elif role in ["user", "assistant", "system"]:
396
+ _render_role_based_dict_message(
397
+ message=message,
398
+ role=str(role),
399
+ index=index,
400
+ console=console,
401
+ timestamp=timestamp,
402
+ chat_width=chat_width,
403
+ )
404
+ else:
405
+ _render_unknown_message(message, index, console, timestamp, chat_width)
406
+
407
+
408
+ def _render_unknown_message(
409
+ message: object,
410
+ index: int | None,
411
+ console: Console,
412
+ timestamp: str | None,
413
+ chat_width: int,
414
+ ) -> None:
415
+ """渲染未知类型的消息 - 居中显示的红色气泡。"""
416
+ title_parts = ["❓ Unknown"]
417
+ if index is not None:
418
+ title_parts.append(f"#{index}")
419
+ if timestamp:
420
+ title_parts.append(f"[dim]{timestamp}[/dim]")
421
+
422
+ title = " ".join(title_parts)
423
+
424
+ # 尝试将消息转换为可读格式 - 显示完整内容
425
+ try:
426
+ content = str(message.model_dump()) if hasattr(message, "model_dump") else str(message) # type: ignore[attr-defined]
427
+ except Exception:
428
+ content = str(message)
429
+
430
+ console.print(
431
+ Panel(
432
+ content,
433
+ title=title,
434
+ title_align="center",
435
+ border_style="red",
436
+ padding=(0, 1),
437
+ width=min(len(content) + 10, chat_width),
438
+ ),
439
+ justify="center",
440
+ )
441
+
442
+
443
+ def create_chat_summary_table(messages: RunnerMessages) -> Table:
444
+ """
445
+ 创建聊天记录摘要表格。
446
+
447
+ Args:
448
+ messages: 要汇总的消息列表
449
+
450
+ Returns:
451
+ Rich Table 对象,包含消息统计信息
452
+ """
453
+ table = Table(title="Chat Summary")
454
+ table.add_column("Message Type", style="cyan")
455
+ table.add_column("Count", justify="right", style="green")
456
+
457
+ # 统计各种消息类型
458
+ counts = {
459
+ "User": 0,
460
+ "Assistant": 0,
461
+ "System": 0,
462
+ "Function Call": 0,
463
+ "Function Output": 0,
464
+ "Unknown": 0,
465
+ }
466
+
467
+ for message in messages:
468
+ if isinstance(message, AgentUserMessage) or (isinstance(message, dict) and message.get("role") == "user"):
469
+ counts["User"] += 1
470
+ elif isinstance(message, AgentAssistantMessage) or (isinstance(message, dict) and message.get("role") == "assistant"):
471
+ counts["Assistant"] += 1
472
+ elif isinstance(message, AgentSystemMessage) or (isinstance(message, dict) and message.get("role") == "system"):
473
+ counts["System"] += 1
474
+ elif isinstance(message, AgentFunctionToolCallMessage) or (isinstance(message, dict) and message.get("type") == "function_call"):
475
+ counts["Function Call"] += 1
476
+ elif isinstance(message, AgentFunctionCallOutput) or (isinstance(message, dict) and message.get("type") == "function_call_output"):
477
+ counts["Function Output"] += 1
478
+ else:
479
+ counts["Unknown"] += 1
480
+
481
+ # 只显示计数大于0的类型
482
+ for msg_type, count in counts.items():
483
+ if count > 0:
484
+ table.add_row(msg_type, str(count))
485
+
486
+ table.add_row("[bold]Total[/bold]", f"[bold]{len(messages)}[/bold]")
487
+
488
+ return table
489
+
490
+
491
+ def print_chat_summary(messages: RunnerMessages, *, console: Console | None = None) -> None:
492
+ """
493
+ 打印聊天记录摘要。
494
+
495
+ Args:
496
+ messages: 要汇总的消息列表
497
+ console: Rich Console 实例,如果为 None 则创建新的
498
+ """
499
+ if console is None:
500
+ console = Console()
501
+
502
+ summary_table = create_chat_summary_table(messages)
503
+ console.print(summary_table)