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.
- {lite_agent-0.4.0 → lite_agent-0.4.1}/CHANGELOG.md +8 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/PKG-INFO +1 -1
- {lite_agent-0.4.0 → lite_agent-0.4.1}/pyproject.toml +1 -1
- {lite_agent-0.4.0 → lite_agent-0.4.1}/src/lite_agent/agent.py +5 -3
- {lite_agent-0.4.0 → lite_agent-0.4.1}/src/lite_agent/runner.py +126 -126
- {lite_agent-0.4.0 → lite_agent-0.4.1}/src/lite_agent/stream_handlers/litellm.py +16 -7
- lite_agent-0.4.1/tests/unit/test_agent_additional.py +182 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/tests/unit/test_agent_handoffs.py +5 -5
- lite_agent-0.4.1/tests/unit/test_chat_display.py +247 -0
- lite_agent-0.4.1/tests/unit/test_chat_display_additional.py +304 -0
- lite_agent-0.4.1/tests/unit/test_message_transfers_additional.py +334 -0
- lite_agent-0.4.1/tests/unit/test_response_event_processor.py +635 -0
- lite_agent-0.4.1/tests/unit/test_simple_stream_handlers.py +56 -0
- lite_agent-0.4.1/tests/unit/test_stream_handlers_additional.py +262 -0
- lite_agent-0.4.0/tests/unit/test_chat_display.py +0 -38
- {lite_agent-0.4.0 → lite_agent-0.4.1}/.claude/settings.local.json +0 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/.github/workflows/ci.yml +0 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/.gitignore +0 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/.python-version +0 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/.vscode/launch.json +0 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/CLAUDE.md +0 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/README.md +0 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/examples/basic.py +0 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/examples/basic_agent.py +0 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/examples/channels/rich_channel.py +0 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/examples/chat_display_demo.py +0 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/examples/confirm_and_continue.py +0 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/examples/consolidate_history.py +0 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/examples/context.py +0 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/examples/handoffs.py +0 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/examples/image.py +0 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/examples/message_transfer_example.py +0 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/examples/message_transfer_example_new.py +0 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/examples/new_message_structure_demo.py +0 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/examples/response_api_example.py +0 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/examples/responses.py +0 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/examples/set_chat_history_example.py +0 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/examples/stop_with_tool_call.py +0 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/examples/terminal.py +0 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/examples/translate/main.py +0 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/examples/translate/prompts/translation_system.md.j2 +0 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/examples/translate.py +0 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/examples/type_system_example.py +0 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/scripts/record_chat_messages.py +0 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/src/lite_agent/__init__.py +0 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/src/lite_agent/chat_display.py +0 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/src/lite_agent/client.py +0 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/src/lite_agent/loggers.py +0 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/src/lite_agent/message_transfers.py +0 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/src/lite_agent/processors/__init__.py +0 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/src/lite_agent/processors/completion_event_processor.py +0 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/src/lite_agent/processors/response_event_processor.py +0 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/src/lite_agent/py.typed +0 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/src/lite_agent/stream_handlers/__init__.py +0 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/src/lite_agent/templates/handoffs_source_instructions.xml.j2 +0 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/src/lite_agent/templates/handoffs_target_instructions.xml.j2 +0 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/src/lite_agent/templates/wait_for_user_instructions.xml.j2 +0 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/src/lite_agent/types/__init__.py +0 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/src/lite_agent/types/events.py +0 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/src/lite_agent/types/messages.py +0 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/src/lite_agent/types/tool_calls.py +0 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/tests/integration/test_agent_with_mocks.py +0 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/tests/integration/test_basic.py +0 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/tests/integration/test_mock_litellm.py +0 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/tests/mocks/basic/1.jsonl +0 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/tests/mocks/confirm_and_continue/1.jsonl +0 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/tests/mocks/confirm_and_continue/2.jsonl +0 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/tests/mocks/context/1.jsonl +0 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/tests/mocks/handoffs/1.jsonl +0 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/tests/performance/test_set_chat_history_performance.py +0 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/tests/test_new_messages.py +0 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/tests/unit/test_agent.py +0 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/tests/unit/test_append_message.py +0 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/tests/unit/test_completion_condition.py +0 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/tests/unit/test_file_recording.py +0 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/tests/unit/test_litellm_stream_handler.py +0 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/tests/unit/test_message_transfer.py +0 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/tests/unit/test_message_transfers.py +0 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/tests/unit/test_response_api_format.py +0 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/tests/unit/test_runner.py +0 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/tests/unit/test_set_chat_history.py +0 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/tests/unit/test_stream_chunk_processor.py +0 -0
- {lite_agent-0.4.0 → lite_agent-0.4.1}/tests/utils/mock_litellm.py +0 -0
- {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)
|
|
@@ -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(
|
|
178
|
-
|
|
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=
|
|
66
|
-
meta=meta,
|
|
61
|
+
content=[AssistantTextContent(text=content)],
|
|
62
|
+
meta=meta or AssistantMessageMeta(),
|
|
67
63
|
)
|
|
68
64
|
|
|
69
|
-
def
|
|
70
|
-
"""
|
|
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
|
-
|
|
75
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
#
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
current_message.meta.
|
|
287
|
-
|
|
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
|
|
381
|
-
"""
|
|
382
|
-
|
|
383
|
-
|
|
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
|
|
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
|
|
404
|
-
|
|
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
|
-
|
|
409
|
-
|
|
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
|
-
|
|
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=
|
|
430
|
+
index=i,
|
|
429
431
|
)
|
|
430
|
-
|
|
431
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|