lite-agent 0.4.0__tar.gz → 0.4.1__tar.gz

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.

Files changed (84) hide show
  1. {lite_agent-0.4.0 → lite_agent-0.4.1}/CHANGELOG.md +8 -0
  2. {lite_agent-0.4.0 → lite_agent-0.4.1}/PKG-INFO +1 -1
  3. {lite_agent-0.4.0 → lite_agent-0.4.1}/pyproject.toml +1 -1
  4. {lite_agent-0.4.0 → lite_agent-0.4.1}/src/lite_agent/agent.py +5 -3
  5. {lite_agent-0.4.0 → lite_agent-0.4.1}/src/lite_agent/runner.py +126 -126
  6. {lite_agent-0.4.0 → lite_agent-0.4.1}/src/lite_agent/stream_handlers/litellm.py +16 -7
  7. lite_agent-0.4.1/tests/unit/test_agent_additional.py +182 -0
  8. {lite_agent-0.4.0 → lite_agent-0.4.1}/tests/unit/test_agent_handoffs.py +5 -5
  9. lite_agent-0.4.1/tests/unit/test_chat_display.py +247 -0
  10. lite_agent-0.4.1/tests/unit/test_chat_display_additional.py +304 -0
  11. lite_agent-0.4.1/tests/unit/test_message_transfers_additional.py +334 -0
  12. lite_agent-0.4.1/tests/unit/test_response_event_processor.py +635 -0
  13. lite_agent-0.4.1/tests/unit/test_simple_stream_handlers.py +56 -0
  14. lite_agent-0.4.1/tests/unit/test_stream_handlers_additional.py +262 -0
  15. lite_agent-0.4.0/tests/unit/test_chat_display.py +0 -38
  16. {lite_agent-0.4.0 → lite_agent-0.4.1}/.claude/settings.local.json +0 -0
  17. {lite_agent-0.4.0 → lite_agent-0.4.1}/.github/workflows/ci.yml +0 -0
  18. {lite_agent-0.4.0 → lite_agent-0.4.1}/.gitignore +0 -0
  19. {lite_agent-0.4.0 → lite_agent-0.4.1}/.python-version +0 -0
  20. {lite_agent-0.4.0 → lite_agent-0.4.1}/.vscode/launch.json +0 -0
  21. {lite_agent-0.4.0 → lite_agent-0.4.1}/CLAUDE.md +0 -0
  22. {lite_agent-0.4.0 → lite_agent-0.4.1}/README.md +0 -0
  23. {lite_agent-0.4.0 → lite_agent-0.4.1}/examples/basic.py +0 -0
  24. {lite_agent-0.4.0 → lite_agent-0.4.1}/examples/basic_agent.py +0 -0
  25. {lite_agent-0.4.0 → lite_agent-0.4.1}/examples/channels/rich_channel.py +0 -0
  26. {lite_agent-0.4.0 → lite_agent-0.4.1}/examples/chat_display_demo.py +0 -0
  27. {lite_agent-0.4.0 → lite_agent-0.4.1}/examples/confirm_and_continue.py +0 -0
  28. {lite_agent-0.4.0 → lite_agent-0.4.1}/examples/consolidate_history.py +0 -0
  29. {lite_agent-0.4.0 → lite_agent-0.4.1}/examples/context.py +0 -0
  30. {lite_agent-0.4.0 → lite_agent-0.4.1}/examples/handoffs.py +0 -0
  31. {lite_agent-0.4.0 → lite_agent-0.4.1}/examples/image.py +0 -0
  32. {lite_agent-0.4.0 → lite_agent-0.4.1}/examples/message_transfer_example.py +0 -0
  33. {lite_agent-0.4.0 → lite_agent-0.4.1}/examples/message_transfer_example_new.py +0 -0
  34. {lite_agent-0.4.0 → lite_agent-0.4.1}/examples/new_message_structure_demo.py +0 -0
  35. {lite_agent-0.4.0 → lite_agent-0.4.1}/examples/response_api_example.py +0 -0
  36. {lite_agent-0.4.0 → lite_agent-0.4.1}/examples/responses.py +0 -0
  37. {lite_agent-0.4.0 → lite_agent-0.4.1}/examples/set_chat_history_example.py +0 -0
  38. {lite_agent-0.4.0 → lite_agent-0.4.1}/examples/stop_with_tool_call.py +0 -0
  39. {lite_agent-0.4.0 → lite_agent-0.4.1}/examples/terminal.py +0 -0
  40. {lite_agent-0.4.0 → lite_agent-0.4.1}/examples/translate/main.py +0 -0
  41. {lite_agent-0.4.0 → lite_agent-0.4.1}/examples/translate/prompts/translation_system.md.j2 +0 -0
  42. {lite_agent-0.4.0 → lite_agent-0.4.1}/examples/translate.py +0 -0
  43. {lite_agent-0.4.0 → lite_agent-0.4.1}/examples/type_system_example.py +0 -0
  44. {lite_agent-0.4.0 → lite_agent-0.4.1}/scripts/record_chat_messages.py +0 -0
  45. {lite_agent-0.4.0 → lite_agent-0.4.1}/src/lite_agent/__init__.py +0 -0
  46. {lite_agent-0.4.0 → lite_agent-0.4.1}/src/lite_agent/chat_display.py +0 -0
  47. {lite_agent-0.4.0 → lite_agent-0.4.1}/src/lite_agent/client.py +0 -0
  48. {lite_agent-0.4.0 → lite_agent-0.4.1}/src/lite_agent/loggers.py +0 -0
  49. {lite_agent-0.4.0 → lite_agent-0.4.1}/src/lite_agent/message_transfers.py +0 -0
  50. {lite_agent-0.4.0 → lite_agent-0.4.1}/src/lite_agent/processors/__init__.py +0 -0
  51. {lite_agent-0.4.0 → lite_agent-0.4.1}/src/lite_agent/processors/completion_event_processor.py +0 -0
  52. {lite_agent-0.4.0 → lite_agent-0.4.1}/src/lite_agent/processors/response_event_processor.py +0 -0
  53. {lite_agent-0.4.0 → lite_agent-0.4.1}/src/lite_agent/py.typed +0 -0
  54. {lite_agent-0.4.0 → lite_agent-0.4.1}/src/lite_agent/stream_handlers/__init__.py +0 -0
  55. {lite_agent-0.4.0 → lite_agent-0.4.1}/src/lite_agent/templates/handoffs_source_instructions.xml.j2 +0 -0
  56. {lite_agent-0.4.0 → lite_agent-0.4.1}/src/lite_agent/templates/handoffs_target_instructions.xml.j2 +0 -0
  57. {lite_agent-0.4.0 → lite_agent-0.4.1}/src/lite_agent/templates/wait_for_user_instructions.xml.j2 +0 -0
  58. {lite_agent-0.4.0 → lite_agent-0.4.1}/src/lite_agent/types/__init__.py +0 -0
  59. {lite_agent-0.4.0 → lite_agent-0.4.1}/src/lite_agent/types/events.py +0 -0
  60. {lite_agent-0.4.0 → lite_agent-0.4.1}/src/lite_agent/types/messages.py +0 -0
  61. {lite_agent-0.4.0 → lite_agent-0.4.1}/src/lite_agent/types/tool_calls.py +0 -0
  62. {lite_agent-0.4.0 → lite_agent-0.4.1}/tests/integration/test_agent_with_mocks.py +0 -0
  63. {lite_agent-0.4.0 → lite_agent-0.4.1}/tests/integration/test_basic.py +0 -0
  64. {lite_agent-0.4.0 → lite_agent-0.4.1}/tests/integration/test_mock_litellm.py +0 -0
  65. {lite_agent-0.4.0 → lite_agent-0.4.1}/tests/mocks/basic/1.jsonl +0 -0
  66. {lite_agent-0.4.0 → lite_agent-0.4.1}/tests/mocks/confirm_and_continue/1.jsonl +0 -0
  67. {lite_agent-0.4.0 → lite_agent-0.4.1}/tests/mocks/confirm_and_continue/2.jsonl +0 -0
  68. {lite_agent-0.4.0 → lite_agent-0.4.1}/tests/mocks/context/1.jsonl +0 -0
  69. {lite_agent-0.4.0 → lite_agent-0.4.1}/tests/mocks/handoffs/1.jsonl +0 -0
  70. {lite_agent-0.4.0 → lite_agent-0.4.1}/tests/performance/test_set_chat_history_performance.py +0 -0
  71. {lite_agent-0.4.0 → lite_agent-0.4.1}/tests/test_new_messages.py +0 -0
  72. {lite_agent-0.4.0 → lite_agent-0.4.1}/tests/unit/test_agent.py +0 -0
  73. {lite_agent-0.4.0 → lite_agent-0.4.1}/tests/unit/test_append_message.py +0 -0
  74. {lite_agent-0.4.0 → lite_agent-0.4.1}/tests/unit/test_completion_condition.py +0 -0
  75. {lite_agent-0.4.0 → lite_agent-0.4.1}/tests/unit/test_file_recording.py +0 -0
  76. {lite_agent-0.4.0 → lite_agent-0.4.1}/tests/unit/test_litellm_stream_handler.py +0 -0
  77. {lite_agent-0.4.0 → lite_agent-0.4.1}/tests/unit/test_message_transfer.py +0 -0
  78. {lite_agent-0.4.0 → lite_agent-0.4.1}/tests/unit/test_message_transfers.py +0 -0
  79. {lite_agent-0.4.0 → lite_agent-0.4.1}/tests/unit/test_response_api_format.py +0 -0
  80. {lite_agent-0.4.0 → lite_agent-0.4.1}/tests/unit/test_runner.py +0 -0
  81. {lite_agent-0.4.0 → lite_agent-0.4.1}/tests/unit/test_set_chat_history.py +0 -0
  82. {lite_agent-0.4.0 → lite_agent-0.4.1}/tests/unit/test_stream_chunk_processor.py +0 -0
  83. {lite_agent-0.4.0 → lite_agent-0.4.1}/tests/utils/mock_litellm.py +0 -0
  84. {lite_agent-0.4.0 → lite_agent-0.4.1}/uv.lock +0 -0
@@ -1,3 +1,11 @@
1
+ ## v0.4.1
2
+
3
+ [v0.4.0...v0.4.1](https://github.com/Jannchie/lite-agent/compare/v0.4.0...v0.4.1)
4
+
5
+ ### :art: Refactors
6
+
7
+ - **runner**: replace if-elif with match-case for chunk handling - By [Jannchie](mailto:jannchie@gmail.com) in [e9a8464](https://github.com/Jannchie/lite-agent/commit/e9a8464)
8
+
1
9
  ## v0.4.0
2
10
 
3
11
  [v0.3.0...v0.4.0](https://github.com/Jannchie/lite-agent/compare/v0.3.0...v0.4.0)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lite-agent
3
- Version: 0.4.0
3
+ Version: 0.4.1
4
4
  Summary: A lightweight, extensible framework for building AI agent.
5
5
  Author-email: Jianqi Pan <jannchie@gmail.com>
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "lite-agent"
3
- version = "0.4.0"
3
+ version = "0.4.1"
4
4
  description = "A lightweight, extensible framework for building AI agent."
5
5
  readme = "README.md"
6
6
  authors = [{ name = "Jianqi Pan", email = "jannchie@gmail.com" }]
@@ -174,9 +174,11 @@ class Agent:
174
174
  if self.completion_condition == "call":
175
175
  instructions = WAIT_FOR_USER_INSTRUCTIONS_TEMPLATE.render(extra_instructions=None) + "\n\n" + instructions
176
176
  return [
177
- system_message_to_llm_dict(NewSystemMessage(
178
- content=f"You are {self.name}. {instructions}",
179
- )),
177
+ system_message_to_llm_dict(
178
+ NewSystemMessage(
179
+ content=f"You are {self.name}. {instructions}",
180
+ ),
181
+ ),
180
182
  *converted_messages,
181
183
  ]
182
184
 
@@ -30,6 +30,7 @@ from lite_agent.types import (
30
30
  UserMessageContent,
31
31
  UserTextContent,
32
32
  )
33
+ from lite_agent.types.events import AssistantMessageEvent
33
34
 
34
35
  DEFAULT_INCLUDES: tuple[AgentChunkType, ...] = (
35
36
  "completion_raw",
@@ -56,38 +57,31 @@ class Runner:
56
57
 
57
58
  def _start_assistant_message(self, content: str = "", meta: AssistantMessageMeta | None = None) -> None:
58
59
  """Start a new assistant message."""
59
- if meta is None:
60
- meta = AssistantMessageMeta()
61
-
62
- # Always add text content, even if empty (we can update it later)
63
- assistant_content_items: list[AssistantMessageContent] = [AssistantTextContent(text=content)]
64
60
  self._current_assistant_message = NewAssistantMessage(
65
- content=assistant_content_items,
66
- meta=meta,
61
+ content=[AssistantTextContent(text=content)],
62
+ meta=meta or AssistantMessageMeta(),
67
63
  )
68
64
 
69
- def _add_to_current_assistant_message(self, content_item: AssistantTextContent | AssistantToolCall | AssistantToolCallResult) -> None:
70
- """Add content to the current assistant message."""
65
+ def _ensure_current_assistant_message(self) -> NewAssistantMessage:
66
+ """Ensure current assistant message exists and return it."""
71
67
  if self._current_assistant_message is None:
72
68
  self._start_assistant_message()
69
+ return self._current_assistant_message # type: ignore[return-value]
73
70
 
74
- if self._current_assistant_message is not None:
75
- self._current_assistant_message.content.append(content_item)
71
+ def _add_to_current_assistant_message(self, content_item: AssistantTextContent | AssistantToolCall | AssistantToolCallResult) -> None:
72
+ """Add content to the current assistant message."""
73
+ self._ensure_current_assistant_message().content.append(content_item)
76
74
 
77
75
  def _add_text_content_to_current_assistant_message(self, delta: str) -> None:
78
76
  """Add text delta to the current assistant message's text content."""
79
- if self._current_assistant_message is None:
80
- self._start_assistant_message()
81
-
82
- if self._current_assistant_message is not None:
83
- # Find the first text content item and append the delta
84
- for content_item in self._current_assistant_message.content:
85
- if content_item.type == "text":
86
- content_item.text += delta
87
- return
88
- # If no text content found, add new text content
89
- new_content = AssistantTextContent(text=delta)
90
- self._current_assistant_message.content.append(new_content)
77
+ message = self._ensure_current_assistant_message()
78
+ # Find the first text content item and append the delta
79
+ for content_item in message.content:
80
+ if content_item.type == "text":
81
+ content_item.text += delta
82
+ return
83
+ # If no text content found, add new text content
84
+ message.content.append(AssistantTextContent(text=delta))
91
85
 
92
86
  def _finalize_assistant_message(self) -> None:
93
87
  """Finalize the current assistant message and add it to messages."""
@@ -131,7 +125,7 @@ class Runner:
131
125
  for i, tool_call in enumerate(transfer_calls):
132
126
  if i == 0:
133
127
  # Execute the first transfer
134
- await self._handle_agent_transfer(tool_call, includes)
128
+ await self._handle_agent_transfer(tool_call)
135
129
  else:
136
130
  # Add response for additional transfer calls without executing them
137
131
  self._add_tool_call_result(
@@ -146,7 +140,7 @@ class Runner:
146
140
  for i, tool_call in enumerate(return_parent_calls):
147
141
  if i == 0:
148
142
  # Execute the first transfer
149
- await self._handle_parent_transfer(tool_call, includes)
143
+ await self._handle_parent_transfer(tool_call)
150
144
  else:
151
145
  # Add response for additional transfer calls without executing them
152
146
  self._add_tool_call_result(
@@ -184,17 +178,16 @@ class Runner:
184
178
  ) -> AsyncGenerator[AgentChunk, None]:
185
179
  """Run the agent and return a RunResponse object that can be asynchronously iterated for each chunk."""
186
180
  includes = self._normalize_includes(includes)
187
- if isinstance(user_input, str):
188
- user_message = NewUserMessage(content=[UserTextContent(text=user_input)])
189
- self.messages.append(user_message)
190
- elif isinstance(user_input, (list, tuple)):
191
- # Handle sequence of messages
192
- for message in user_input:
193
- self.append_message(message)
194
- else:
195
- # Handle single message (BaseModel, TypedDict, or dict)
196
- # Type assertion needed due to the complex union type
197
- self.append_message(user_input) # type: ignore[arg-type]
181
+ match user_input:
182
+ case str():
183
+ self.messages.append(NewUserMessage(content=[UserTextContent(text=user_input)]))
184
+ case list() | tuple():
185
+ # Handle sequence of messages
186
+ for message in user_input:
187
+ self.append_message(message)
188
+ case _:
189
+ # Handle single message (BaseModel, TypedDict, or dict)
190
+ self.append_message(user_input) # type: ignore[arg-type]
198
191
  return self._run(max_steps, includes, self._normalize_record_path(record_to), context=context)
199
192
 
200
193
  async def _run(self, max_steps: int, includes: Sequence[AgentChunkType], record_to: Path | None = None, context: Any | None = None) -> AsyncGenerator[AgentChunk, None]: # noqa: ANN401
@@ -229,62 +222,79 @@ class Runner:
229
222
  msg = f"Unknown API type: {self.api}"
230
223
  raise ValueError(msg)
231
224
  async for chunk in resp:
232
- if chunk.type in includes:
233
- yield chunk
234
- if chunk.type == "assistant_message":
235
- # Start or update assistant message in new format
236
- meta = AssistantMessageMeta(
237
- sent_at=chunk.message.meta.sent_at,
238
- latency_ms=getattr(chunk.message.meta, "latency_ms", None),
239
- total_time_ms=getattr(chunk.message.meta, "output_time_ms", None),
240
- )
241
- # If we already have a current assistant message, just update its metadata
242
- if self._current_assistant_message is not None:
243
- self._current_assistant_message.meta = meta
244
- else:
245
- # Extract text content from the new message format
246
- text_content = ""
247
- if chunk.message.content:
248
- for item in chunk.message.content:
249
- if hasattr(item, "type") and item.type == "text":
250
- text_content = item.text
251
- break
252
- self._start_assistant_message(text_content, meta)
253
- if chunk.type == "content_delta":
254
- # Accumulate text content to current assistant message
255
- self._add_text_content_to_current_assistant_message(chunk.delta)
256
- if chunk.type == "function_call":
257
- # Add tool call to current assistant message
258
- # Keep arguments as string for compatibility with funcall library
259
- tool_call = AssistantToolCall(
260
- call_id=chunk.call_id,
261
- name=chunk.name,
262
- arguments=chunk.arguments or "{}",
263
- )
264
- self._add_to_current_assistant_message(tool_call)
265
- if chunk.type == "usage":
266
- # Update the last assistant message with usage data and output_time_ms
267
- usage_time = datetime.now(timezone.utc)
268
- for i in range(len(self.messages) - 1, -1, -1):
269
- current_message = self.messages[i]
270
- if isinstance(current_message, NewAssistantMessage):
271
- # Update usage information
272
- if current_message.meta.usage is None:
273
- current_message.meta.usage = MessageUsage()
274
- current_message.meta.usage.input_tokens = chunk.usage.input_tokens
275
- current_message.meta.usage.output_tokens = chunk.usage.output_tokens
276
- current_message.meta.usage.total_tokens = (chunk.usage.input_tokens or 0) + (chunk.usage.output_tokens or 0)
277
-
278
- # Calculate output_time_ms if latency_ms is available
279
- if current_message.meta.latency_ms is not None:
280
- # We need to calculate from first output to usage time
281
- # We'll calculate: usage_time - (sent_at - latency_ms)
282
- # This gives us the time from first output to usage completion
283
- # sent_at is when the message was completed, so sent_at - latency_ms approximates first output time
284
- first_output_time_approx = current_message.meta.sent_at - timedelta(milliseconds=current_message.meta.latency_ms)
285
- output_time_ms = int((usage_time - first_output_time_approx).total_seconds() * 1000)
286
- current_message.meta.total_time_ms = max(0, output_time_ms)
287
- break
225
+ match chunk.type:
226
+ case "assistant_message":
227
+ # Start or update assistant message in new format
228
+ meta = AssistantMessageMeta(
229
+ sent_at=chunk.message.meta.sent_at,
230
+ latency_ms=getattr(chunk.message.meta, "latency_ms", None),
231
+ total_time_ms=getattr(chunk.message.meta, "output_time_ms", None),
232
+ )
233
+ # If we already have a current assistant message, just update its metadata
234
+ if self._current_assistant_message is not None:
235
+ self._current_assistant_message.meta = meta
236
+ else:
237
+ # Extract text content from the new message format
238
+ text_content = ""
239
+ if chunk.message.content:
240
+ for item in chunk.message.content:
241
+ if hasattr(item, "type") and item.type == "text":
242
+ text_content = item.text
243
+ break
244
+ self._start_assistant_message(text_content, meta)
245
+ # Only yield assistant_message chunk if it's in includes and has content
246
+ if chunk.type in includes and self._current_assistant_message is not None:
247
+ # Create a new chunk with the current assistant message content
248
+ updated_chunk = AssistantMessageEvent(
249
+ message=self._current_assistant_message,
250
+ )
251
+ yield updated_chunk
252
+ case "content_delta":
253
+ # Accumulate text content to current assistant message
254
+ self._add_text_content_to_current_assistant_message(chunk.delta)
255
+ # Always yield content_delta chunk if it's in includes
256
+ if chunk.type in includes:
257
+ yield chunk
258
+ case "function_call":
259
+ # Add tool call to current assistant message
260
+ # Keep arguments as string for compatibility with funcall library
261
+ tool_call = AssistantToolCall(
262
+ call_id=chunk.call_id,
263
+ name=chunk.name,
264
+ arguments=chunk.arguments or "{}",
265
+ )
266
+ self._add_to_current_assistant_message(tool_call)
267
+ # Always yield function_call chunk if it's in includes
268
+ if chunk.type in includes:
269
+ yield chunk
270
+ case "usage":
271
+ # Update the last assistant message with usage data and output_time_ms
272
+ usage_time = datetime.now(timezone.utc)
273
+ for i in range(len(self.messages) - 1, -1, -1):
274
+ current_message = self.messages[i]
275
+ if isinstance(current_message, NewAssistantMessage):
276
+ # Update usage information
277
+ if current_message.meta.usage is None:
278
+ current_message.meta.usage = MessageUsage()
279
+ current_message.meta.usage.input_tokens = chunk.usage.input_tokens
280
+ current_message.meta.usage.output_tokens = chunk.usage.output_tokens
281
+ current_message.meta.usage.total_tokens = (chunk.usage.input_tokens or 0) + (chunk.usage.output_tokens or 0)
282
+
283
+ # Calculate output_time_ms if latency_ms is available
284
+ if current_message.meta.latency_ms is not None:
285
+ # We need to calculate from first output to usage time
286
+ # We'll calculate: usage_time - (sent_at - latency_ms)
287
+ # This gives us the time from first output to usage completion
288
+ # sent_at is when the message was completed, so sent_at - latency_ms approximates first output time
289
+ first_output_time_approx = current_message.meta.sent_at - timedelta(milliseconds=current_message.meta.latency_ms)
290
+ output_time_ms = int((usage_time - first_output_time_approx).total_seconds() * 1000)
291
+ current_message.meta.total_time_ms = max(0, output_time_ms)
292
+ break
293
+ # Always yield usage chunk if it's in includes
294
+ if chunk.type in includes:
295
+ yield chunk
296
+ case _ if chunk.type in includes:
297
+ yield chunk
288
298
 
289
299
  # Finalize assistant message so it can be found in pending function calls
290
300
  self._finalize_assistant_message()
@@ -377,58 +387,50 @@ class Runner:
377
387
  resp = self.run(user_input, max_steps, includes, record_to=record_to)
378
388
  return await self._collect_all_chunks(resp)
379
389
 
380
- def _find_pending_tool_calls(self) -> list[AssistantToolCall]:
381
- """Find tool calls that don't have corresponding results yet."""
382
- # Find pending calls directly in new format messages
383
- pending_calls: list[AssistantToolCall] = []
384
-
385
- # Look at the last assistant message for pending tool calls
386
- if not self.messages:
387
- return pending_calls
388
-
389
- last_message = self.messages[-1]
390
- if not isinstance(last_message, NewAssistantMessage):
391
- return pending_calls
390
+ def _analyze_last_assistant_message(self) -> tuple[list[AssistantToolCall], dict[str, str]]:
391
+ """Analyze the last assistant message and return pending tool calls and tool call map."""
392
+ if not self.messages or not isinstance(self.messages[-1], NewAssistantMessage):
393
+ return [], {}
392
394
 
393
- # Collect tool calls and results from the last assistant message
394
395
  tool_calls = {}
395
396
  tool_results = set()
397
+ tool_call_names = {}
396
398
 
397
- for content_item in last_message.content:
399
+ for content_item in self.messages[-1].content:
398
400
  if content_item.type == "tool_call":
399
401
  tool_calls[content_item.call_id] = content_item
402
+ tool_call_names[content_item.call_id] = content_item.name
400
403
  elif content_item.type == "tool_call_result":
401
404
  tool_results.add(content_item.call_id)
402
405
 
403
- # Return tool calls that don't have corresponding results
404
- return [call for call_id, call in tool_calls.items() if call_id not in tool_results]
406
+ # Return pending tool calls and tool call names map
407
+ pending_calls = [call for call_id, call in tool_calls.items() if call_id not in tool_results]
408
+ return pending_calls, tool_call_names
409
+
410
+ def _find_pending_tool_calls(self) -> list[AssistantToolCall]:
411
+ """Find tool calls that don't have corresponding results yet."""
412
+ pending_calls, _ = self._analyze_last_assistant_message()
413
+ return pending_calls
405
414
 
406
415
  def _get_tool_call_name_by_id(self, call_id: str) -> str | None:
407
416
  """Get the tool name for a given call_id from the last assistant message."""
408
- if not self.messages or not isinstance(self.messages[-1], NewAssistantMessage):
409
- return None
410
-
411
- for content_item in self.messages[-1].content:
412
- if content_item.type == "tool_call" and content_item.call_id == call_id:
413
- return content_item.name
414
- return None
417
+ _, tool_call_names = self._analyze_last_assistant_message()
418
+ return tool_call_names.get(call_id)
415
419
 
416
420
  def _convert_tool_calls_to_tool_calls(self, tool_calls: list[AssistantToolCall]) -> list[ToolCall]:
417
421
  """Convert AssistantToolCall objects to ToolCall objects for compatibility."""
418
-
419
- result_tool_calls = []
420
- for tc in tool_calls:
421
- tool_call = ToolCall(
422
+ return [
423
+ ToolCall(
422
424
  id=tc.call_id,
423
425
  type="function",
424
426
  function=ToolCallFunction(
425
427
  name=tc.name,
426
428
  arguments=tc.arguments if isinstance(tc.arguments, str) else str(tc.arguments),
427
429
  ),
428
- index=len(result_tool_calls),
430
+ index=i,
429
431
  )
430
- result_tool_calls.append(tool_call)
431
- return result_tool_calls
432
+ for i, tc in enumerate(tool_calls)
433
+ ]
432
434
 
433
435
  def set_chat_history(self, messages: Sequence[FlexibleRunnerMessage], root_agent: Agent | None = None) -> None:
434
436
  """Set the entire chat history and track the current agent based on function calls.
@@ -691,12 +693,11 @@ class Runner:
691
693
  msg = f"Unsupported message type: {type(message)}"
692
694
  raise TypeError(msg)
693
695
 
694
- async def _handle_agent_transfer(self, tool_call: ToolCall, _includes: Sequence[AgentChunkType]) -> None:
696
+ async def _handle_agent_transfer(self, tool_call: ToolCall) -> None:
695
697
  """Handle agent transfer when transfer_to_agent tool is called.
696
698
 
697
699
  Args:
698
700
  tool_call: The transfer_to_agent tool call
699
- _includes: The types of chunks to include in output (unused)
700
701
  """
701
702
 
702
703
  # Parse the arguments to get the target agent name
@@ -771,12 +772,11 @@ class Runner:
771
772
  output=f"Transfer failed: {e!s}",
772
773
  )
773
774
 
774
- async def _handle_parent_transfer(self, tool_call: ToolCall, _includes: Sequence[AgentChunkType]) -> None:
775
+ async def _handle_parent_transfer(self, tool_call: ToolCall) -> None:
775
776
  """Handle parent transfer when transfer_to_parent tool is called.
776
777
 
777
778
  Args:
778
779
  tool_call: The transfer_to_parent tool call
779
- _includes: The types of chunks to include in output (unused)
780
780
  """
781
781
 
782
782
  # Check if current agent has a parent
@@ -16,18 +16,27 @@ if TYPE_CHECKING:
16
16
  from aiofiles.threadpool.text import AsyncTextIOWrapper
17
17
 
18
18
 
19
- def ensure_record_file(record_to: Path | None) -> Path | None:
19
+ def ensure_record_file(record_to: Path | str | None) -> Path | None:
20
20
  if not record_to:
21
21
  return None
22
- if not record_to.parent.exists():
23
- logger.warning('Record directory "%s" does not exist, creating it.', record_to.parent)
24
- record_to.parent.mkdir(parents=True, exist_ok=True)
25
- return record_to
22
+
23
+ path = Path(record_to) if isinstance(record_to, str) else record_to
24
+
25
+ # If the path is a directory, generate a filename
26
+ if path.is_dir():
27
+ path = path / "conversation.jsonl"
28
+
29
+ # Ensure parent directory exists
30
+ if not path.parent.exists():
31
+ logger.warning('Record directory "%s" does not exist, creating it.', path.parent)
32
+ path.parent.mkdir(parents=True, exist_ok=True)
33
+
34
+ return path
26
35
 
27
36
 
28
37
  async def litellm_completion_stream_handler(
29
38
  resp: litellm.CustomStreamWrapper,
30
- record_to: Path | None = None,
39
+ record_to: Path | str | None = None,
31
40
  ) -> AsyncGenerator[AgentChunk, None]:
32
41
  """
33
42
  Optimized chunk handler
@@ -52,7 +61,7 @@ async def litellm_completion_stream_handler(
52
61
 
53
62
  async def litellm_response_stream_handler(
54
63
  resp: AsyncGenerator[ResponsesAPIStreamingResponse, None],
55
- record_to: Path | None = None,
64
+ record_to: Path | str | None = None,
56
65
  ) -> AsyncGenerator[AgentChunk, None]:
57
66
  """
58
67
  Response API stream handler for processing ResponsesAPIStreamingResponse chunks
@@ -0,0 +1,182 @@
1
+ """
2
+ 为agent.py未覆盖部分添加的额外测试
3
+ """
4
+
5
+ from lite_agent.agent import Agent
6
+ from lite_agent.types import (
7
+ AgentAssistantMessage,
8
+ AgentUserMessage,
9
+ FlexibleRunnerMessage,
10
+ NewSystemMessage,
11
+ NewUserMessage,
12
+ RunnerMessage,
13
+ UserTextContent,
14
+ )
15
+
16
+
17
+ class TestAgentAdditional:
18
+ """Agent类的额外测试"""
19
+
20
+ def test_agent_init_with_handoffs(self):
21
+ """测试带交接代理的初始化"""
22
+ child_agent = Agent(model="gpt-3", name="Child", instructions="Child instructions")
23
+ parent_agent = Agent(
24
+ model="gpt-3",
25
+ name="Parent",
26
+ instructions="Parent instructions",
27
+ handoffs=[child_agent],
28
+ )
29
+ assert child_agent.parent is parent_agent
30
+ assert parent_agent.handoffs == [child_agent]
31
+
32
+ def test_agent_init_with_completion_condition(self):
33
+ """测试带完成条件的初始化"""
34
+ agent = Agent(
35
+ model="gpt-3",
36
+ name="TestBot",
37
+ instructions="Be helpful.",
38
+ completion_condition="call",
39
+ )
40
+ assert agent.completion_condition == "call"
41
+
42
+ def test_prepare_completion_messages_with_complex_messages(self):
43
+ """测试处理复杂消息格式"""
44
+ agent = Agent(model="gpt-3", name="TestBot", instructions="Be helpful.")
45
+
46
+ # 包含函数调用的消息
47
+ messages: list[FlexibleRunnerMessage] = [
48
+ AgentUserMessage(content=[UserTextContent(text="Hello")]),
49
+ AgentAssistantMessage(content=[]),
50
+ {
51
+ "type": "function_call",
52
+ "call_id": "call_123",
53
+ "name": "test_function",
54
+ "arguments": '{"param": "value"}',
55
+ },
56
+ {
57
+ "type": "function_call_output",
58
+ "call_id": "call_123",
59
+ "output": "Function result",
60
+ },
61
+ ]
62
+
63
+ result = agent.prepare_completion_messages(messages)
64
+
65
+ # 应该包含系统消息
66
+ assert result[0]["role"] == "system"
67
+
68
+ # 应该正确处理各种消息类型
69
+ assert len(result) >= 3
70
+
71
+ def test_prepare_completion_messages_with_new_message_format(self):
72
+ """测试新消息格式的处理"""
73
+ agent = Agent(model="gpt-3", name="TestBot", instructions="Be helpful.")
74
+
75
+ messages: list[RunnerMessage] = [
76
+ NewUserMessage(content=[UserTextContent(text="New user message")]),
77
+ NewSystemMessage(content="New system message"),
78
+ ]
79
+
80
+ result = agent.prepare_completion_messages(messages)
81
+
82
+ # 应该正确转换新格式消息
83
+ assert result[0]["role"] == "system" # 代理的系统消息
84
+ assert len(result) >= 2
85
+
86
+ def test_agent_with_tools_registration(self):
87
+ """测试工具注册"""
88
+
89
+ def test_tool(param: str) -> str:
90
+ return f"Result: {param}"
91
+
92
+ agent = Agent(
93
+ model="gpt-3",
94
+ name="TestBot",
95
+ instructions="Be helpful.",
96
+ tools=[test_tool],
97
+ )
98
+
99
+ # 工具应该被注册
100
+ assert agent.fc is not None
101
+ tools = agent.fc.get_tools()
102
+ assert len(tools) > 0 # 至少包含注册的工具
103
+
104
+ def test_agent_handoff_tools(self):
105
+ """测试代理交接工具的注册"""
106
+ child_agent = Agent(model="gpt-3", name="Child", instructions="Child instructions")
107
+ parent_agent = Agent(
108
+ model="gpt-3",
109
+ name="Parent",
110
+ instructions="Parent instructions",
111
+ handoffs=[child_agent],
112
+ )
113
+
114
+ # 验证父子关系建立
115
+ assert child_agent.parent is parent_agent
116
+ assert parent_agent.handoffs == [child_agent]
117
+
118
+ # 验证工具注册系统工作
119
+ assert parent_agent.fc is not None
120
+ assert child_agent.fc is not None
121
+
122
+ def test_agent_wait_for_user_tool(self):
123
+ """测试等待用户工具的注册"""
124
+ agent = Agent(
125
+ model="gpt-3",
126
+ name="TestBot",
127
+ instructions="Be helpful.",
128
+ completion_condition="call",
129
+ )
130
+
131
+ # 验证completion_condition设置正确
132
+ assert agent.completion_condition == "call"
133
+
134
+ # 验证funcall系统正常工作
135
+ assert agent.fc is not None
136
+
137
+ def test_agent_client_property(self):
138
+ """测试代理客户端属性"""
139
+ agent = Agent(model="gpt-3", name="TestBot", instructions="Be helpful.")
140
+
141
+ # 应该有client属性
142
+ assert agent.client is not None
143
+ assert hasattr(agent.client, "model")
144
+
145
+ def test_message_conversion_edge_cases(self):
146
+ """测试消息转换的边界情况"""
147
+ agent = Agent(model="gpt-3", name="TestBot", instructions="Be helpful.")
148
+
149
+ # 测试空消息列表
150
+ result = agent.prepare_completion_messages([])
151
+ assert len(result) == 1 # 只有系统消息
152
+ assert result[0]["role"] == "system"
153
+
154
+ # 测试只有系统消息的情况
155
+ messages = [{"role": "system", "content": "Custom system message"}]
156
+ result = agent.prepare_completion_messages(messages)
157
+ assert len(result) >= 1
158
+
159
+ def test_agent_basic_properties(self):
160
+ """测试代理基本属性"""
161
+ agent = Agent(model="gpt-3", name="TestBot", instructions="Be helpful.")
162
+
163
+ assert agent.name == "TestBot"
164
+ assert agent.instructions == "Be helpful."
165
+ assert agent.completion_condition == "stop" # 默认值
166
+ assert agent.handoffs == []
167
+ assert agent.parent is None
168
+
169
+ def test_agent_with_custom_system_message(self):
170
+ """测试自定义系统消息的处理"""
171
+ agent = Agent(model="gpt-3", name="TestBot", instructions="Be helpful.")
172
+
173
+ messages = [
174
+ {"role": "system", "content": "Custom system instruction"},
175
+ AgentUserMessage(content=[UserTextContent(text="Hello")]),
176
+ ]
177
+
178
+ result = agent.prepare_completion_messages(messages)
179
+
180
+ # 应该保留自定义系统消息
181
+ system_messages = [msg for msg in result if msg["role"] == "system"]
182
+ assert len(system_messages) >= 1