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
lite_agent/__init__.py
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"""Lite Agent - A lightweight AI agent framework."""
|
|
2
2
|
|
|
3
3
|
from .agent import Agent
|
|
4
|
+
from .chat_display import display_chat_summary, display_messages
|
|
4
5
|
from .message_transfers import consolidate_history_transfer
|
|
5
|
-
from .rich_helpers import print_chat_history, print_chat_summary
|
|
6
6
|
from .runner import Runner
|
|
7
7
|
|
|
8
|
-
__all__ = ["Agent", "Runner", "consolidate_history_transfer", "
|
|
8
|
+
__all__ = ["Agent", "Runner", "consolidate_history_transfer", "display_chat_summary", "display_messages"]
|
lite_agent/agent.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import time
|
|
1
2
|
from collections.abc import AsyncGenerator, Callable, Sequence
|
|
2
3
|
from pathlib import Path
|
|
3
4
|
from typing import Any, Optional
|
|
@@ -5,12 +6,12 @@ from typing import Any, Optional
|
|
|
5
6
|
from funcall import Funcall
|
|
6
7
|
from jinja2 import Environment, FileSystemLoader
|
|
7
8
|
from litellm import CustomStreamWrapper
|
|
8
|
-
from pydantic import BaseModel
|
|
9
9
|
|
|
10
10
|
from lite_agent.client import BaseLLMClient, LiteLLMClient
|
|
11
11
|
from lite_agent.loggers import logger
|
|
12
|
-
from lite_agent.stream_handlers import
|
|
13
|
-
from lite_agent.types import AgentChunk,
|
|
12
|
+
from lite_agent.stream_handlers import litellm_completion_stream_handler, litellm_response_stream_handler
|
|
13
|
+
from lite_agent.types import AgentChunk, FunctionCallEvent, FunctionCallOutputEvent, RunnerMessages, ToolCall, message_to_llm_dict, system_message_to_llm_dict
|
|
14
|
+
from lite_agent.types.messages import NewAssistantMessage, NewSystemMessage, NewUserMessage
|
|
14
15
|
|
|
15
16
|
TEMPLATES_DIR = Path(__file__).parent / "templates"
|
|
16
17
|
jinja_env = Environment(loader=FileSystemLoader(str(TEMPLATES_DIR)), autoescape=True)
|
|
@@ -162,41 +163,120 @@ class Agent:
|
|
|
162
163
|
# Regenerate transfer tools to include the new agent
|
|
163
164
|
self._add_transfer_tools(self.handoffs)
|
|
164
165
|
|
|
165
|
-
def prepare_completion_messages(self, messages: RunnerMessages) -> list[dict
|
|
166
|
-
|
|
166
|
+
def prepare_completion_messages(self, messages: RunnerMessages) -> list[dict]:
|
|
167
|
+
"""Prepare messages for completions API (with conversion)."""
|
|
167
168
|
converted_messages = self._convert_responses_to_completions_format(messages)
|
|
168
|
-
|
|
169
|
-
# Prepare instructions with handoff-specific additions
|
|
170
169
|
instructions = self.instructions
|
|
171
|
-
|
|
172
|
-
# Add source instructions if this agent can handoff to others
|
|
173
170
|
if self.handoffs:
|
|
174
171
|
instructions = HANDOFFS_SOURCE_INSTRUCTIONS_TEMPLATE.render(extra_instructions=None) + "\n\n" + instructions
|
|
175
|
-
|
|
176
|
-
# Add target instructions if this agent can be handed off to (has a parent)
|
|
177
172
|
if self.parent:
|
|
178
173
|
instructions = HANDOFFS_TARGET_INSTRUCTIONS_TEMPLATE.render(extra_instructions=None) + "\n\n" + instructions
|
|
179
|
-
|
|
180
|
-
# Add wait_for_user instructions if completion condition is "call"
|
|
181
174
|
if self.completion_condition == "call":
|
|
182
175
|
instructions = WAIT_FOR_USER_INSTRUCTIONS_TEMPLATE.render(extra_instructions=None) + "\n\n" + instructions
|
|
183
|
-
|
|
184
176
|
return [
|
|
185
|
-
|
|
186
|
-
role="system",
|
|
177
|
+
system_message_to_llm_dict(NewSystemMessage(
|
|
187
178
|
content=f"You are {self.name}. {instructions}",
|
|
188
|
-
)
|
|
179
|
+
)),
|
|
189
180
|
*converted_messages,
|
|
190
181
|
]
|
|
191
182
|
|
|
183
|
+
def prepare_responses_messages(self, messages: RunnerMessages) -> list[dict[str, Any]]:
|
|
184
|
+
"""Prepare messages for responses API (no conversion, just add system message if needed)."""
|
|
185
|
+
instructions = self.instructions
|
|
186
|
+
if self.handoffs:
|
|
187
|
+
instructions = HANDOFFS_SOURCE_INSTRUCTIONS_TEMPLATE.render(extra_instructions=None) + "\n\n" + instructions
|
|
188
|
+
if self.parent:
|
|
189
|
+
instructions = HANDOFFS_TARGET_INSTRUCTIONS_TEMPLATE.render(extra_instructions=None) + "\n\n" + instructions
|
|
190
|
+
if self.completion_condition == "call":
|
|
191
|
+
instructions = WAIT_FOR_USER_INSTRUCTIONS_TEMPLATE.render(extra_instructions=None) + "\n\n" + instructions
|
|
192
|
+
res: list[dict[str, Any]] = [
|
|
193
|
+
{
|
|
194
|
+
"role": "system",
|
|
195
|
+
"content": f"You are {self.name}. {instructions}",
|
|
196
|
+
},
|
|
197
|
+
]
|
|
198
|
+
for message in messages:
|
|
199
|
+
if isinstance(message, NewAssistantMessage):
|
|
200
|
+
for item in message.content:
|
|
201
|
+
match item.type:
|
|
202
|
+
case "text":
|
|
203
|
+
res.append(
|
|
204
|
+
{
|
|
205
|
+
"role": "assistant",
|
|
206
|
+
"content": item.text,
|
|
207
|
+
},
|
|
208
|
+
)
|
|
209
|
+
case "tool_call":
|
|
210
|
+
res.append(
|
|
211
|
+
{
|
|
212
|
+
"type": "function_call",
|
|
213
|
+
"call_id": item.call_id,
|
|
214
|
+
"name": item.name,
|
|
215
|
+
"arguments": item.arguments,
|
|
216
|
+
},
|
|
217
|
+
)
|
|
218
|
+
case "tool_call_result":
|
|
219
|
+
res.append(
|
|
220
|
+
{
|
|
221
|
+
"type": "function_call_output",
|
|
222
|
+
"call_id": item.call_id,
|
|
223
|
+
"output": item.output,
|
|
224
|
+
},
|
|
225
|
+
)
|
|
226
|
+
elif isinstance(message, NewSystemMessage):
|
|
227
|
+
res.append(
|
|
228
|
+
{
|
|
229
|
+
"role": "system",
|
|
230
|
+
"content": message.content,
|
|
231
|
+
},
|
|
232
|
+
)
|
|
233
|
+
elif isinstance(message, NewUserMessage):
|
|
234
|
+
contents = []
|
|
235
|
+
for item in message.content:
|
|
236
|
+
match item.type:
|
|
237
|
+
case "text":
|
|
238
|
+
contents.append(
|
|
239
|
+
{
|
|
240
|
+
"type": "input_text",
|
|
241
|
+
"text": item.text,
|
|
242
|
+
},
|
|
243
|
+
)
|
|
244
|
+
case "image":
|
|
245
|
+
contents.append(
|
|
246
|
+
{
|
|
247
|
+
"type": "input_image",
|
|
248
|
+
"image_url": item.image_url,
|
|
249
|
+
},
|
|
250
|
+
)
|
|
251
|
+
case "file":
|
|
252
|
+
contents.append(
|
|
253
|
+
{
|
|
254
|
+
"type": "input_file",
|
|
255
|
+
"file_id": item.file_id,
|
|
256
|
+
"file_name": item.file_name,
|
|
257
|
+
},
|
|
258
|
+
)
|
|
259
|
+
res.append(
|
|
260
|
+
{
|
|
261
|
+
"role": message.role,
|
|
262
|
+
"content": contents,
|
|
263
|
+
},
|
|
264
|
+
)
|
|
265
|
+
# Handle dict messages (legacy format)
|
|
266
|
+
elif isinstance(message, dict):
|
|
267
|
+
res.append(message)
|
|
268
|
+
return res
|
|
269
|
+
|
|
192
270
|
async def completion(self, messages: RunnerMessages, record_to_file: Path | None = None) -> AsyncGenerator[AgentChunk, None]:
|
|
193
|
-
# Apply message transfer callback if provided
|
|
271
|
+
# Apply message transfer callback if provided - always use legacy format for LLM compatibility
|
|
194
272
|
processed_messages = messages
|
|
195
273
|
if self.message_transfer:
|
|
196
274
|
logger.debug(f"Applying message transfer callback for agent {self.name}")
|
|
197
275
|
processed_messages = self.message_transfer(messages)
|
|
198
276
|
|
|
277
|
+
# For completions API, use prepare_completion_messages
|
|
199
278
|
self.message_histories = self.prepare_completion_messages(processed_messages)
|
|
279
|
+
|
|
200
280
|
tools = self.fc.get_tools(target="completion")
|
|
201
281
|
resp = await self.client.completion(
|
|
202
282
|
messages=self.message_histories,
|
|
@@ -206,10 +286,27 @@ class Agent:
|
|
|
206
286
|
|
|
207
287
|
# Ensure resp is a CustomStreamWrapper
|
|
208
288
|
if isinstance(resp, CustomStreamWrapper):
|
|
209
|
-
return
|
|
289
|
+
return litellm_completion_stream_handler(resp, record_to=record_to_file)
|
|
210
290
|
msg = "Response is not a CustomStreamWrapper, cannot stream chunks."
|
|
211
291
|
raise TypeError(msg)
|
|
212
292
|
|
|
293
|
+
async def responses(self, messages: RunnerMessages, record_to_file: Path | None = None) -> AsyncGenerator[AgentChunk, None]:
|
|
294
|
+
# Apply message transfer callback if provided - always use legacy format for LLM compatibility
|
|
295
|
+
processed_messages = messages
|
|
296
|
+
if self.message_transfer:
|
|
297
|
+
logger.debug(f"Applying message transfer callback for agent {self.name}")
|
|
298
|
+
processed_messages = self.message_transfer(messages)
|
|
299
|
+
|
|
300
|
+
# For responses API, use prepare_responses_messages (no conversion)
|
|
301
|
+
self.message_histories = self.prepare_responses_messages(processed_messages)
|
|
302
|
+
tools = self.fc.get_tools()
|
|
303
|
+
resp = await self.client.responses(
|
|
304
|
+
messages=self.message_histories,
|
|
305
|
+
tools=tools,
|
|
306
|
+
tool_choice="auto", # TODO: make this configurable
|
|
307
|
+
)
|
|
308
|
+
return litellm_response_stream_handler(resp, record_to=record_to_file)
|
|
309
|
+
|
|
213
310
|
async def list_require_confirm_tools(self, tool_calls: Sequence[ToolCall] | None) -> Sequence[ToolCall]:
|
|
214
311
|
if not tool_calls:
|
|
215
312
|
return []
|
|
@@ -225,7 +322,7 @@ class Agent:
|
|
|
225
322
|
results.append(tool_call)
|
|
226
323
|
return results
|
|
227
324
|
|
|
228
|
-
async def handle_tool_calls(self, tool_calls: Sequence[ToolCall] | None, context: Any | None = None) -> AsyncGenerator[
|
|
325
|
+
async def handle_tool_calls(self, tool_calls: Sequence[ToolCall] | None, context: Any | None = None) -> AsyncGenerator[FunctionCallEvent | FunctionCallOutputEvent, None]: # noqa: ANN401
|
|
229
326
|
if not tool_calls:
|
|
230
327
|
return
|
|
231
328
|
if tool_calls:
|
|
@@ -236,26 +333,31 @@ class Agent:
|
|
|
236
333
|
continue
|
|
237
334
|
|
|
238
335
|
for tool_call in tool_calls:
|
|
336
|
+
yield FunctionCallEvent(
|
|
337
|
+
call_id=tool_call.id,
|
|
338
|
+
name=tool_call.function.name,
|
|
339
|
+
arguments=tool_call.function.arguments or "",
|
|
340
|
+
)
|
|
341
|
+
start_time = time.time()
|
|
239
342
|
try:
|
|
240
|
-
yield ToolCallChunk(
|
|
241
|
-
type="tool_call",
|
|
242
|
-
name=tool_call.function.name,
|
|
243
|
-
arguments=tool_call.function.arguments or "",
|
|
244
|
-
)
|
|
245
343
|
content = await self.fc.call_function_async(tool_call.function.name, tool_call.function.arguments or "", context)
|
|
246
|
-
|
|
247
|
-
|
|
344
|
+
end_time = time.time()
|
|
345
|
+
execution_time_ms = int((end_time - start_time) * 1000)
|
|
346
|
+
yield FunctionCallOutputEvent(
|
|
248
347
|
tool_call_id=tool_call.id,
|
|
249
348
|
name=tool_call.function.name,
|
|
250
349
|
content=str(content),
|
|
350
|
+
execution_time_ms=execution_time_ms,
|
|
251
351
|
)
|
|
252
|
-
except Exception as e:
|
|
352
|
+
except Exception as e:
|
|
253
353
|
logger.exception("Tool call %s failed", tool_call.id)
|
|
254
|
-
|
|
255
|
-
|
|
354
|
+
end_time = time.time()
|
|
355
|
+
execution_time_ms = int((end_time - start_time) * 1000)
|
|
356
|
+
yield FunctionCallOutputEvent(
|
|
256
357
|
tool_call_id=tool_call.id,
|
|
257
358
|
name=tool_call.function.name,
|
|
258
359
|
content=str(e),
|
|
360
|
+
execution_time_ms=execution_time_ms,
|
|
259
361
|
)
|
|
260
362
|
|
|
261
363
|
def _convert_responses_to_completions_format(self, messages: RunnerMessages) -> list[dict]:
|
|
@@ -265,7 +367,7 @@ class Agent:
|
|
|
265
367
|
|
|
266
368
|
while i < len(messages):
|
|
267
369
|
message = messages[i]
|
|
268
|
-
message_dict = message
|
|
370
|
+
message_dict = message_to_llm_dict(message) if isinstance(message, (NewUserMessage, NewSystemMessage, NewAssistantMessage)) else message
|
|
269
371
|
|
|
270
372
|
message_type = message_dict.get("type")
|
|
271
373
|
role = message_dict.get("role")
|
|
@@ -277,11 +379,11 @@ class Agent:
|
|
|
277
379
|
|
|
278
380
|
while j < len(messages):
|
|
279
381
|
next_message = messages[j]
|
|
280
|
-
next_dict = next_message
|
|
382
|
+
next_dict = message_to_llm_dict(next_message) if isinstance(next_message, (NewUserMessage, NewSystemMessage, NewAssistantMessage)) else next_message
|
|
281
383
|
|
|
282
384
|
if next_dict.get("type") == "function_call":
|
|
283
385
|
tool_call = {
|
|
284
|
-
"id": next_dict["
|
|
386
|
+
"id": next_dict["call_id"], # type: ignore
|
|
285
387
|
"type": "function",
|
|
286
388
|
"function": {
|
|
287
389
|
"name": next_dict["name"], # type: ignore
|
|
@@ -340,44 +442,52 @@ class Agent:
|
|
|
340
442
|
|
|
341
443
|
converted_content = []
|
|
342
444
|
for item in content:
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
"text": item["text"],
|
|
351
|
-
},
|
|
352
|
-
)
|
|
353
|
-
elif item_type == "input_image":
|
|
354
|
-
# Convert ResponseInputImage to completion API format
|
|
355
|
-
if item.get("file_id"):
|
|
356
|
-
msg = "File ID input is not supported for Completion API. Please use image_url instead of file_id for image input."
|
|
357
|
-
raise ValueError(msg)
|
|
358
|
-
|
|
359
|
-
if not item.get("image_url"):
|
|
360
|
-
msg = "ResponseInputImage must have either file_id or image_url, but image_url is required for Completion API."
|
|
361
|
-
raise ValueError(msg)
|
|
362
|
-
|
|
363
|
-
# Build image_url object with detail inside
|
|
364
|
-
image_data = {"url": item["image_url"]}
|
|
365
|
-
detail = item.get("detail", "auto")
|
|
366
|
-
if detail: # Include detail if provided
|
|
367
|
-
image_data["detail"] = detail
|
|
368
|
-
|
|
369
|
-
converted_content.append(
|
|
370
|
-
{
|
|
371
|
-
"type": "image_url",
|
|
372
|
-
"image_url": image_data,
|
|
373
|
-
},
|
|
374
|
-
)
|
|
375
|
-
else:
|
|
376
|
-
# Keep existing format (text, image_url)
|
|
377
|
-
converted_content.append(item)
|
|
445
|
+
# Convert Pydantic objects to dict first
|
|
446
|
+
if hasattr(item, "model_dump"):
|
|
447
|
+
item_dict = item.model_dump()
|
|
448
|
+
elif hasattr(item, "dict"): # For older Pydantic versions
|
|
449
|
+
item_dict = item.dict()
|
|
450
|
+
elif isinstance(item, dict):
|
|
451
|
+
item_dict = item
|
|
378
452
|
else:
|
|
379
453
|
# Handle non-dict items (shouldn't happen, but just in case)
|
|
380
454
|
converted_content.append(item)
|
|
455
|
+
continue
|
|
456
|
+
|
|
457
|
+
item_type = item_dict.get("type")
|
|
458
|
+
if item_type in ["input_text", "text"]:
|
|
459
|
+
# Convert ResponseInputText or new text format to completion API format
|
|
460
|
+
converted_content.append(
|
|
461
|
+
{
|
|
462
|
+
"type": "text",
|
|
463
|
+
"text": item_dict["text"],
|
|
464
|
+
},
|
|
465
|
+
)
|
|
466
|
+
elif item_type in ["input_image", "image"]:
|
|
467
|
+
# Convert ResponseInputImage to completion API format
|
|
468
|
+
if item_dict.get("file_id"):
|
|
469
|
+
msg = "File ID input is not supported for Completion API"
|
|
470
|
+
raise ValueError(msg)
|
|
471
|
+
|
|
472
|
+
if not item_dict.get("image_url"):
|
|
473
|
+
msg = "ResponseInputImage must have either file_id or image_url"
|
|
474
|
+
raise ValueError(msg)
|
|
475
|
+
|
|
476
|
+
# Build image_url object with detail inside
|
|
477
|
+
image_data = {"url": item_dict["image_url"]}
|
|
478
|
+
detail = item_dict.get("detail", "auto")
|
|
479
|
+
if detail: # Include detail if provided
|
|
480
|
+
image_data["detail"] = detail
|
|
481
|
+
|
|
482
|
+
converted_content.append(
|
|
483
|
+
{
|
|
484
|
+
"type": "image_url",
|
|
485
|
+
"image_url": image_data,
|
|
486
|
+
},
|
|
487
|
+
)
|
|
488
|
+
else:
|
|
489
|
+
# Keep existing format (text, image_url)
|
|
490
|
+
converted_content.append(item_dict)
|
|
381
491
|
|
|
382
492
|
return converted_content
|
|
383
493
|
|