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 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", "print_chat_history", "print_chat_summary"]
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 litellm_stream_handler
13
- from lite_agent.types import AgentChunk, AgentSystemMessage, RunnerMessages, ToolCall, ToolCallChunk, ToolCallResultChunk
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[str, str]]:
166
- # Convert from responses format to completions format
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
- AgentSystemMessage(
186
- role="system",
177
+ system_message_to_llm_dict(NewSystemMessage(
187
178
  content=f"You are {self.name}. {instructions}",
188
- ).model_dump(),
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 litellm_stream_handler(resp, record_to=record_to_file)
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[ToolCallChunk | ToolCallResultChunk, None]: # noqa: ANN401
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
- yield ToolCallResultChunk(
247
- type="tool_call_result",
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: # noqa: PERF203
352
+ except Exception as e:
253
353
  logger.exception("Tool call %s failed", tool_call.id)
254
- yield ToolCallResultChunk(
255
- type="tool_call_result",
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.model_dump() if isinstance(message, BaseModel) else 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.model_dump() if isinstance(next_message, BaseModel) else 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["function_call_id"], # type: ignore
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
- if isinstance(item, dict):
344
- item_type = item.get("type")
345
- if item_type == "input_text":
346
- # Convert ResponseInputText to completion API format
347
- converted_content.append(
348
- {
349
- "type": "text",
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